How to obtain Spring actuator /health metrics that already working?
For example, to push them to Grafana. Thus I need them as objects, not text.
You can inject properties which are exposed through actuator endpoints (like /health) by injecting instance of endpoint class using actuator API.
This stackoverflow answer explains about it in detail:
Does Spring Boot Actuator have a Java API?
Take a look on this example
package ru.formatko;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.prometheus.client.Collector;
import io.prometheus.client.exporter.common.TextFormat;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.Status;
import org.springframework.stereotype.Component;
/**
* Example:
* # HELP health_status HealthCheck result in prometheus's response format
* # TYPE health_status gauge
* health_status{application="java-service",type="main",} 1.0
* health_status{application="java-service",type="db",database="PostgreSQL",validationQuery="isValid()",} 1.0
* health_status{application="java-service",type="diskSpace",total="506332180480",exists="true",threshold="10485760",free="412188921856",} 1.0
* health_status{application="java-service",type="ping",} 1.0
*/
#Component
#Endpoint(id = "health-check")
public class HeathPrometheusEndpoint {
private static final String APPLICATION = "application";
private static final String TYPE = "type";
public static final String SAMPLE_HEALTH_STATUS = "health_status";
private final HealthEndpoint healthEndpoint;
private final String appName;
private final ObjectMapper mapper;
private final HttpCodeStatusMapper httpCodeStatusMapper;
public HeathPrometheusEndpoint(HealthEndpoint healthEndpoint,
ObjectMapper mapper,
#Value("${spring.application.name:}") String appName,
HttpCodeStatusMapper httpCodeStatusMapper) {
this.healthEndpoint = healthEndpoint;
this.mapper = mapper;
this.appName = appName;
this.httpCodeStatusMapper = httpCodeStatusMapper;
}
#ReadOperation(produces = TextFormat.CONTENT_TYPE_004)
public WebEndpointResponse<String> healthPrometheus() {
StatusDto status = createStatusDto();
List<Collector.MetricFamilySamples.Sample> samples = new ArrayList<>();
samples.add(createMainSample(status));
samples.addAll(createComponentSamples(status));
return createStringWebEndpointResponse(status, createMetricFamily(samples));
}
#SneakyThrows
private StatusDto createStatusDto() {
return mapper.readValue(mapper.writeValueAsString(healthEndpoint.health()), StatusDto.class);
}
private Collector.MetricFamilySamples.Sample createMainSample(StatusDto status) {
Labels labels = new Labels();
labels.add(APPLICATION, appName);
labels.add(TYPE, "main");
return createSample(SAMPLE_HEALTH_STATUS, labels, status.getStatus());
}
private List<Collector.MetricFamilySamples.Sample> createComponentSamples(StatusDto status) {
List<Collector.MetricFamilySamples.Sample> list = new ArrayList<>();
for (Map.Entry<String, StatusDto> entry : status.components.entrySet()) {
Labels labels = new Labels();
labels.add(APPLICATION, appName);
labels.add(TYPE, entry.getKey());
StatusDto statusDto = entry.getValue();
Map<String, Object> details = statusDto.getDetails();
if (details != null && !details.isEmpty()) {
details.forEach((k, v) -> labels.add(k, String.valueOf(v)));
}
list.add(createSample(SAMPLE_HEALTH_STATUS, labels, statusDto.getStatus()));
}
return list;
}
private Collector.MetricFamilySamples.Sample createSample(String name, Labels labels, Status status) {
double v = Status.UP.equals(status) ? 1 : 0;
return new Collector.MetricFamilySamples.Sample(name, labels.getLabels(), labels.getValues(), v);
}
private Collector.MetricFamilySamples createMetricFamily(List<Collector.MetricFamilySamples.Sample> s) {
return new Collector.MetricFamilySamples(
"health_status", Collector.Type.GAUGE,
"HealthCheck result in prometheus's response format", s);
}
private WebEndpointResponse<String> createStringWebEndpointResponse(
StatusDto status, Collector.MetricFamilySamples metricFamilySamples
) {
try {
Writer writer = new StringWriter();
TextFormat.write004(writer,
Collections.enumeration(Collections.singletonList(metricFamilySamples)));
return wrapResponse(writer.toString(), status);
} catch (IOException ex) {
// This actually never happens since StringWriter::write() doesn't throw any
// IOException
throw new RuntimeException("Writing metrics failed", ex);
}
}
private WebEndpointResponse<String> wrapResponse(String body, StatusDto status) {
if (body == null || body.isEmpty()) {
return new WebEndpointResponse<>("", 500);
} else {
int statusCode = httpCodeStatusMapper.getStatusCode(status.getStatus());
return new WebEndpointResponse<>(body, statusCode);
}
}
public static class Labels {
private final Map<String, String> map = new HashMap<>();
public void add(String label, String value) {
if (value != null && !value.isEmpty()) {
map.put(label, value);
}
}
public List<String> getLabels() {
return new ArrayList<>(map.keySet());
}
public List<String> getValues() {
return new ArrayList<>(map.values());
}
}
#Data
public static class StatusDto {
private Status status;
private Map<String, StatusDto> components;
private Map<String, Object> details;
}
}
Related
Can someone tell me how to add an attachment using ProducerTemplate?
I have been searching but I can not find an answer to my case.
I am using Camen 2.1 and I have these three clases:
MailSender2.java
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ProducerTemplate;
public class MailSender2 extends TypeMail{
private static final ResourceBundle RES = ResourceBundle.getBundle("mail");
protected static final String MAIL_NOTIFICATION_ENDPOINT=RES.getString("mail.host.location").trim()+":"+RES.getString("mail.port").trim();
private Map<String, Object> header;
public MailSender2() {
this.header=new HashMap<>();
}
public void send(ProducerTemplate template) {
this.header.put("From", this.getT_from());
this.header.put("To", this.getT_to());
this.header.put("Subject", this.getT_subject());
this.header.put(Exchange.CONTENT_TYPE, "text/html; charset=UTF-8");
//this.getF_ficher() <-- I have here the file to attach
//this.getT_ficnon() <-- I have here the name ot the file
//this.getT_ficext() <-- I have here the extension ot the file
template.sendBodyAndHeaders(MAIL_NOTIFICATION_ENDPOINT, this.getT_mensaje(), header);
}
}
TypeMail.java:
public class TypeMail {
private String t_id;
private String t_from;
private String t_to;
private String t_subject;
private String t_mensaje;
private byte[] f_ficher;
private String t_ficnon;
private String t_ficext;
public String getT_id() {
return t_id;
}
public void setT_id(String t_id) {
this.t_id = t_id;
}
public String getT_from() {
return t_from;
}
public void setT_from(String t_from) {
this.t_from = t_from;
}
public String getT_to() {
return t_to;
}
public void setT_to(String t_to) {
this.t_to = t_to;
}
public String getT_subject() {
return t_subject;
}
public void setT_subject(String t_subject) {
this.t_subject = t_subject;
}
public String getT_mensaje() {
return t_mensaje;
}
public void setT_mensaje(String t_mensaje) {
this.t_mensaje = t_mensaje;
}
public byte[] getF_ficher() {
return f_ficher;
}
public void setF_ficher(byte[] f_ficher) {
this.f_ficher = f_ficher;
}
public String getT_ficnon() {
return t_ficnon;
}
public void setT_ficnon(String t_ficnon) {
this.t_ficnon = t_ficnon;
}
public String getT_ficext() {
return t_ficext;
}
public void setT_ficext(String t_ficext) {
this.t_ficext = t_ficext;
}
}
MailCommunicationTransformer.java:
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.ProducerTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ws.soap.client.SoapFaultClientException;
public class MailCommunicationTransformer {
MailSender2 mailSender = null;
static Logger logger = LoggerFactory.getLogger(MailCommunicationTransformer.class);
public MailCommunicationTransformer()
{
}
public MailLog transform(Object actualMessage, Exchange exchange, CamelContext context)
{
mailSender = exchange.getIn().getBody(MailSender2.class);
try {
MailSenderDAO mailSenderDAO = (MailSenderDAO)context.getRegistry().lookup("MailSenderDAO");
mailSenderDAO.validarInput(mailSender);
if (mailSender!=null) {
ProducerTemplate template=exchange.getContext().createProducerTemplate();
try {
mailSender.send(template);
}
catch (Throwable ex) {
ex.printStackTrace();
exchange.setProperty(Exchange.EXCEPTION_CAUGHT,ex);
}
}
}catch (MailException me) {
me.printStackTrace();
exchange.setProperty(Exchange.EXCEPTION_CAUGHT,me);
}
Throwable e = exchange.getProperty(Exchange.EXCEPTION_CAUGHT,
Throwable.class);
String response = "OK";
if (e != null) {
StringBuffer mensaje = new StringBuffer();
if (e instanceof SoapFaultClientException) {
mensaje.append("MAIL fault exception: CLIENT. ");
} else {
mensaje.append("MAIL fault exception: MAIL. ");
}
logger.info("MailCommunicationTransformer",e);
while (e != null) {
e.printStackTrace();
mensaje.append(e.getMessage());
e = e.getCause();
}
response = mensaje.toString();
}
MailLog log = new MailLog(mailSender, response); //, protocolo
return log;
}
}
In TypeMail I have the file in f_ficher, and the fileName (t_ficnon) and extension (t_ficext), but I can not find how to attach this file in MailSender2 before template.sendBodyAndHeaders(.....)
Any help would be very appreciated.
Regards.
Perhaps I don't fully understand your question, but the ProducerTemplate don't know about the message type.
You just send a body and perhaps also headers to an endpoint.
Therefore the body just needs to be a fully constructed MimeMessage object as documented in the Camel Mail docs.
You can simply construct the mail message with Java and then use the object with the ProducerTemplate (what you already do).
template.sendBodyAndHeaders("your-smtp-endpoint", yourMimeMessageInstance, yourHeaderMap);
Thanks for the answer!
But, finally, I could do it this way:
new class EmailProcessor.java
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Objects;
import java.util.ResourceBundle;
import javax.activation.DataHandler;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.commons.codec.binary.Base64;
public class EmailProcessor implements Processor {
// Atributos de la clase
private TypeMail typeMail;
public EmailProcessor(TypeMail typeMail) {
this.typeMail = typeMail;
}
#Override
public void process(Exchange exchange) throws Exception {
Message ms = exchange.getIn();
ms.setHeader("From", this.typeMail.getT_from());
ms.setHeader("To", this.typeMail.getT_to());
ms.setHeader("Subject", this.typeMail.getT_subject());
ms.setHeader(Exchange.CONTENT_TYPE, "text/html; charset=UTF-8");
ms.setBody("<p style='font-family: Calibri;'>" + this.typeMail.getT_mensaje() + "</p>");
if (this.typeMail.getF_ficher() != null) {
String mimeType = "application/pdf";
if ("zip".equals(typeMail.getT_ficext())) {
mimeType = "application/zip";
}
ms.addAttachment(typeMail.getT_ficnom() + "." + typeMail.getT_ficext(), new DataHandler(typeMail.getF_ficher(), mimeType));
}
}
}
MailSender.java:
import java.util.ResourceBundle;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ProducerTemplate;
public class MailSender extends TypeMail{
private static final ResourceBundle RES = ResourceBundle.getBundle("mail");
protected static final String MAIL_NOTIFICATION_ENDPOINT=RES.getString("mail.host.location").trim()+":"+RES.getString("mail.port").trim();
public MailSender() {
}
public void send(ProducerTemplate template) {
template.send(MAIL_NOTIFICATION_ENDPOINT, ExchangePattern.InOnly, new EmailProcessor(this));
}
}
I created a database driven message bean which loads messages from database and if a message key cannot be found then it falls back to properties files.
It works fine except one thing. All other message properties from spring are missing.
(Eg. error messages from spring security AbstractSecurityInterceptor.authenticationNotFound.)
Spring 4.1.6
<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="propertiesMessageSource">
<property name="basenames">
<list>
<value>classpath:events</value>
<value>classpath:messages.prod</value>
<value>classpath:messages</value>
</list>
</property>
</bean>
<bean id="messageSource" class="hu.bluesystem.hrportal.services.DatabaseDrivenMessageSource">
<constructor-arg ref="messageResourceService"/>
<property name="parentMessageSource" ref="propertiesMessageSource"/>
</bean>
package x.x.services;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
public final class DatabaseDrivenMessageSource extends AbstractMessageSource implements ResourceLoaderAware {
final private Logger log = Logger.getLogger(getClass());
private ResourceLoader resourceLoader;
private final Map<String, Map<String, String>> properties = new HashMap();
#Autowired
private MessageResourceServiceImpl messageResourceService;
public DatabaseDrivenMessageSource(MessageResourceServiceImpl messageResourceService) {
this.messageResourceService = messageResourceService;
}
#PostConstruct
public void init() {
reload();
}
#Override
protected MessageFormat resolveCode(String code, Locale locale) {
String msg = getText(code, locale);
MessageFormat result = createMessageFormat(msg, locale);
return result;
}
#Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
String result = getText(code, locale);
return result;
}
private String getText(String code, Locale locale) {
Map<String, String> localized = properties.get(code);
String textForCurrentLanguage = null;
if (localized != null) {
textForCurrentLanguage = localized.get(locale.getLanguage());
if (textForCurrentLanguage == null) {
textForCurrentLanguage = localized.get(new Locale("en_US"));
}
}
if (textForCurrentLanguage == null) {
//Check parent message
try {
textForCurrentLanguage = getParentMessageSource().getMessage(code, null, locale);
} catch (Exception e) {
logger.error("Cannot find message with code: " + code);
}
}
return textForCurrentLanguage != null ? textForCurrentLanguage : code;
}
public void reload() {
properties.clear();
properties.putAll(loadTexts());
}
protected Map<String, Map<String, String>> loadTexts() {
return messageResourceService.reloadMessages();
}
#Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
}
}
package x.x.services;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
public class MessageResourceServiceImpl implements MessageResourceService {
private static final Logger LOG = Logger.getLogger(MessageResourceServiceImpl.class.getName());
String dataSourceName;
Properties dbProperties;
DriverManagerDataSource dataSource;
private Map<String, Map<String, String>> msg = new HashMap();
public Map<String, Map<String, String>> reloadMessages() {
try {
dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(dbProperties.getProperty("jdbc.driverClassName"));
dataSource.setPassword(dbProperties.getProperty("jdbc.password"));
dataSource.setUsername(dbProperties.getProperty("jdbc.username"));
dataSource.setUrl(dbProperties.getProperty("jdbc.url"));
load();
dataSource.getConnection().close();
} catch (Exception ex) {
Logger.getLogger(SpringPropertiesUtilImpl.class.getName()).log(Level.SEVERE, null, ex);
}
return msg;
}
private void load() {
Map<String, String> m;
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List<Map<String, Object>> messages = jdbcTemplate
.queryForList("select msg_key, msg_value,msg_locale from hrp_app_messages where msg_status='true'");
LOG.info("Loading messages from Database");
for(Map<String, Object> message : messages) {
m=new HashMap();
if(msg.get(message.get("MSG_KEY").toString())!=null) {
m=msg.get(message.get("MSG_KEY").toString());
}
m.put(message.get("MSG_LOCALE").toString(),message.get("MSG_VALUE").toString());
msg.put(message.get("MSG_KEY").toString(),m);
}
}
public String getDataSourceName() {
return dataSourceName;
}
public void setDataSourceName(String dataSourceName) {
this.dataSourceName = dataSourceName;
}
public Properties getDbProperties() {
return dbProperties;
}
public void setDbProperties(Properties dbProperties) {
this.dbProperties = dbProperties;
}
}
Using ReloadableResourceBundleMessageSource class instead of AbstractMessageSource it works fine. Thank you for the support.
I am integrating with an old system and have a need to parse the following xml into my object. I am trying to do this with jackson but I can't get the mapping to work. Anyone know how to map the following xml to the pojo?
#JacksonXmlRootElement(localName = "properties")
#Data
public class Example {
private String token;
private String affid;
private String domain;
}
xml example:
<properties>
<entry key="token">rent</entry>
<entry key="affid">true</entry>
<entry key="domain">checking</entry>
</properties>
I have tried adding
#JacksonXmlProperty(isAttribute = true, localName = "key")
to the properties but this of course doesn't work and I do not see another way to get this to work. Any ideas?
I am using the mapper like so...
ObjectMapper xmlMapper = new XmlMapper();
dto = xmlMapper.readValue(XML_STRING, Example .class);
I am using the following dependencies
compile('org.springframework.boot:spring-boot-starter-web')
runtime('org.springframework.boot:spring-boot-devtools')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
compile('org.apache.commons:commons-lang3:3.5')
compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml')
compile('com.squareup.okhttp3:okhttp:3.10.0')
This does work.
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
public class XmlParserDemo {
public static void main(String[] args) throws IOException, XMLStreamException {
String xmlString = "<properties>\n" +
" <entry key=\"token\">rent</entry>\n" +
" <entry key=\"affid\">true</entry>\n" +
" <entry key=\"domain\">checking</entry>\n" +
"</properties>";
XMLStreamReader sr = null;
sr = XMLInputFactory.newFactory().createXMLStreamReader(new StringReader(xmlString));
sr.next();
XmlMapper mapper = new XmlMapper();
List<Entry> entries = mapper.readValue(sr, new TypeReference<List<Entry>>() {
});
sr.close();
entries.forEach(e ->
System.out.println(e.key + ":" + e.value));
}
public static class Entry {
#JacksonXmlProperty(isAttribute = true, localName = "key")
private String key;
#JacksonXmlText
private String value;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
Output is:
token:rent
affid:true
domain:checking
I have looked through Jackson thoroughly and it doesn't seem that there is a way to accomplish this. However, I will share my solution here in case it is useful to someone else.
package com.example.config;
import com.example.dto.Example;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
public class Converter extends AbstractHttpMessageConverter<Example> {
private static final XPath XPATH_INSTANCE = XPathFactory.newInstance().newXPath();
private static final StringHttpMessageConverter MESSAGE_CONVERTER = new StringHttpMessageConverter();
#Override
protected boolean supports(Class<?> aClass) {
return aClass == Example.class;
}
#Override
protected Example readInternal(Class<? extends LongFormDTO> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
String responseString = MESSAGE_CONVERTER.read(String.class, httpInputMessage);
Reader xmlInput = new StringReader(responseString);
InputSource inputSource = new InputSource(xmlInput);
Example dto = new Example();
Node xml;
try {
xml = (Node) XPATH_INSTANCE.evaluate("/properties", inputSource, XPathConstants.NODE);
} catch (XPathExpressionException e) {
log.error("Unable to parse response", e);
return dto;
}
log.info("processing populate application response={}", responseString);
dto.setToken(getString("token", xml));
dto.setAffid(getInt("affid", xml, 36));
dto.domain(getString("domain", xml));
xmlInput.close();
return dto;
}
private String getString(String propName, Node xml, String defaultValue) {
String xpath = String.format("//entry[#key='%s']/text()", propName);
try {
String value = (String) XPATH_INSTANCE.evaluate(xpath, xml, XPathConstants.STRING);
return StringUtils.isEmpty(value) ? defaultValue : value;
} catch (XPathExpressionException e) {
log.error("Received error retrieving property={} from xml", propName, e);
}
return defaultValue;
}
private String getString(String propName, Node xml) {
return getString(propName, xml, null);
}
private int getInt(String propName, Node xml, int defaultValue) {
String stringValue = getString(propName, xml);
if (!StringUtils.isEmpty(stringValue)) {
try {
return Integer.parseInt(stringValue);
} catch (NumberFormatException e) {
log.error("Attempted to parse value={} as integer but received error", stringValue, e);
}
}
return defaultValue;
}
private int getInt(String propName, Node xml) {
return getInt(propName, xml,0);
}
private boolean getBoolean(String propName, Node xml) {
String stringValue = getString(propName, xml );
return Boolean.valueOf(stringValue);
}
#Override
protected void writeInternal(Example dto, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
throw new UnsupportedOperationException("Responses of type=" + MediaType.TEXT_PLAIN_VALUE + " are not supported");
}
}
I chose to hide this in a message converter so I don't have to look at it again but you can apply these steps where you see fit. If you choose this route, you will need to configure a rest template to use this converter. If not, it is important to cache the xml into a Node object as regenerating each time will be very costly.
package com.example.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
#Configuration
public class RestConfig {
#Bean
#Primary
public RestTemplate restTemplate() {
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
#Bean
public RestTemplate restTemplateLe(RestTemplateBuilder builder) {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
ExampleConverter exampleConverter = new ExampleConverter();
exampleConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
messageConverters.add(exampleConverter);
return builder.messageConverters(messageConverters)
.requestFactory(new OkHttp3ClientHttpRequestFactory())
.build();
}
}
I am trying to implement ( for exercise ) , with Java EE , the algorithm of active replication.
I encountered an unexpected problem.
I have this function (pseudo-code):
public boolean doOperation() in the frontEnd EJB{
sendJMSMessageToTopic(); //publish new command to the replica
Thread.sleep(200);
readFromQueue(); //read Ack from replica
control if write quorum is reached
commit or abort
return state (success or not)
}
The problem is:
The callback onMessage() is called but the response is not received in the same invocation of the doOperation.
When I call doOperation again I read the response of previous invocation of doOperation method.
I use glassfish, and i deploy all component in the same application server.
I use a topic for publish the request to the replicas and a queue to receive the responce from the replicas. FrontEnd is Singleton and Replicas are Message Drive Bean.
UPDATE: I
My Replica Code
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package EJBData;
import Service.ResponceMessage;
import Service.Command;
import Service.Product;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.inject.Inject;
import javax.jms.JMSConnectionFactory;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ObjectMessage;
import javax.jms.Queue;
/**
*
* #author melix
*/
#MessageDriven(activationConfig = {
#ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
#ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "TopicRepliche"),
//#ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "durable"),
#ActivationConfigProperty(propertyName = "clientId", propertyValue = "TopicRepliche"),
#ActivationConfigProperty(propertyName = "subscriptionName", propertyValue = "TopicRepliche")
})
public class Replica1 implements MessageListener {
#Resource(mappedName = "QueueFrontEnd")
private Queue queueFrontEnd;
#Inject
#JMSConnectionFactory("java:comp/DefaultJMSConnectionFactory")
private JMSContext context;
private final int R_CONTAINS_KEY=0;
private final int W_REMOVE_KEY=1;
private final int W_PUT=2;
private final int R_GET=3;
private final int R_UPDATE_PRICE=4;
private final int R_GENERATE_JSON_FROM_MAP=5;
private final int COD_REPLICA=0;
private final ConcurrentHashMap<Integer,Product> item_map=new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long,Command> pending_command=new ConcurrentHashMap<>();
private long last_cmd_executed=0;
private long last_cmd_received=0;
private synchronized void setLastCmdExecuted(long last_cmd_executed){
this.last_cmd_executed=last_cmd_executed;
}
private synchronized void setLastCmdReceived(long last_cmd_received){
this.last_cmd_received=last_cmd_received;
}
public boolean containsKey(int key){
return item_map.containsKey(key);
}
public boolean removeKey(int key){
item_map.remove(key);
return true;
}
public boolean put(Product b){
item_map.put(Integer.parseInt(b.getId()),b);
return true;
}
public Product get(int key){
return item_map.get(key);
}
public void updatePrice(){
for (Map.Entry pairs : item_map.entrySet()) {
int key=(int)pairs.getKey();
Product p=(Product)pairs.getValue();
double price=p.getInitialPrice();
item_map.remove(key);
System.out.println(price);
if(price>=5)
p.setInitialPrice(price-5);
else
p.setInitialPrice(0);
item_map.put(key, p);
}
}
public String generateJSONFromMap(){
String json="[";
for (Map.Entry pairs : item_map.entrySet()) {
Product p=(Product)pairs.getValue();
json+=p.toString();
json+=",";
}
if(json.endsWith(","))
json = json.substring(0,json.length() - 1);
json+="]";
return json;
}
public Replica1() {
}
#Override
public void onMessage(Message message) {
Command c;
if (message instanceof ObjectMessage) {
try {
Object object = ((ObjectMessage) message).getObject();
c=(Command)object;
int command_type=c.getType();
Object res=null;
switch(command_type){
case R_CONTAINS_KEY:
res=containsKey((int)c.getInput());
break;
case W_REMOVE_KEY:
res=removeKey((int)c.getInput());
break;
case W_PUT:
res=put((Product)c.getInput());
break;
case R_GET:
res=get((int)c.getInput());
break;
case R_UPDATE_PRICE:
updatePrice();
break;
case R_GENERATE_JSON_FROM_MAP:
res=generateJSONFromMap();
break;
}
System.out.println("FROM REPLICA, ACK FOR"+c.getSqnCommand());
this.setLastCmdReceived(c.getSqnCommand());
sendAckToQueueFrontEnd(c.getSqnCommand(),res);
} catch (JMSException ex) {
Logger.getLogger(Replica1.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
private void sendJMSMessageToQueueFrontEnd(String messageData) {
context.createProducer().send(queueFrontEnd, messageData);
}
private void sendAckToQueueFrontEnd(long sqn_command,Object responce) {
try {
ResponceMessage ack=new ResponceMessage(true,false,sqn_command,COD_REPLICA);
ack.setResponce(responce);
ObjectMessage objectMessage=context.createObjectMessage();
objectMessage.setObject(ack);
context.createProducer().send(queueFrontEnd,objectMessage);
} catch (JMSException ex) {
Logger.getLogger(Replica1.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
The front end code:
(doOperation is mapped with sendNewCommand() in my code)
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package EJBData;
import EJBExecutor.ReaderLocal;
import Service.Command;
import Service.Product;
import Service.ResponceMessage;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
import javax.inject.Inject;
import javax.jms.JMSConnectionFactory;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.ObjectMessage;
import javax.jms.Topic;
/**
*
* #author melix
*/
#Stateless
public class AuctionFrontEnd implements AuctionFrontEndLocal{
#EJB
private FrontEndConsumer frontEndConsumer;
#EJB
private ReaderLocal reader;
#Resource(mappedName = "TopicRepliche")
private Topic topicRepliche;
#Inject
#JMSConnectionFactory("java:comp/DefaultJMSConnectionFactory")
private JMSContext context;
#Resource
TimerService service;
private boolean isConsumerInit=false;
private final int R_CONTAINS_KEY=0;
private final int W_REMOVE_KEY=1;
private final int W_PUT=2;
private final int R_GET=3;
private final int R_UPDATE_PRICE=4;
private final int R_GENERATE_JSON_FROM_MAP=5;
private final ConcurrentHashMap<Integer,Product> item_map=new ConcurrentHashMap<>();
private ArrayList<ConcurrentHashMap<Long,ResponceMessage>> listOfHashMap;
private Queue<Command> pending_command = new LinkedList<Command>();
private final int WRITE_QUORUM=2;
private final int READ_QUORUM=2;
private final int NUM_REPLIC=3;
private int last_key_inserted=0;
private long num_command=0;
public void initConsumer(){
frontEndConsumer.init();
}
private synchronized void putCommandInQueue(Command c){
pending_command.add(c);
}
private synchronized Command removeCommandFromQueue(Command c){
if(!pending_command.isEmpty())
return pending_command.remove();
return null;
}
private synchronized void addAck(int cod_replica,long num_command,ResponceMessage m){
listOfHashMap.get(cod_replica).put(num_command,m);
}
private synchronized void addResponce(int cod_replica,long num_command,ResponceMessage m){
listOfHashMap.get(cod_replica+3).put(num_command,m);
}
#Override
public synchronized void addAckToList(int cod_replica,long num_command,ResponceMessage m){
addAck(cod_replica,num_command,m);
}
#Override
public synchronized void addResponceToList(int cod_replica,long num_command,ResponceMessage m){
addAck(cod_replica,num_command,m);
}
private synchronized long addNumCommand(){
this.num_command++;
return num_command;
}
#Override
public void mexReceived(String message){
System.out.println(message);
}
#Timeout
public void handleTimeout(Timer timer) {
if(!pending_command.isEmpty()){
Command c=pending_command.poll();
for(int i=0;i<NUM_REPLIC*2;i++){
if(listOfHashMap.get(i).containsKey(c.getSqnCommand())){
ResponceMessage m=listOfHashMap.get(i).get(c.getSqnCommand());
System.out.println("Ack per comando:"+m.getSqnCommand()+"from replica"+m.getCode_replica());
}
}
}
timer.cancel();
}
public void startTimer() {
TimerConfig config = new TimerConfig();
config.setPersistent(false);
Timer timer = service.createSingleActionTimer(200, config);
}
private ResponceMessage[] sendNewCommand(boolean isWriteOperation,int type,Object input){
int num_ack=0,i=0;
Command c=new Command(isWriteOperation,addNumCommand(),type,input);
ObjectMessage objectMessage=context.createObjectMessage();
try {
objectMessage.setObject(c);
} catch (JMSException ex) {
Logger.getLogger(AuctionFrontEnd.class.getName()).log(Level.SEVERE, null, ex);
}
if(!isConsumerInit){
this.initConsumer();
isConsumerInit=true;
}
frontEndConsumer.cleanQueue();
sendJMSMessageToTopicRepliche(objectMessage);
ResponceMessage[] m=new ResponceMessage[NUM_REPLIC];
ResponceMessage tmp = null;
do{
tmp=frontEndConsumer.consume();
if(tmp!=null){
System.out.println("ACK CORRECT"+tmp.getSqnCommand()+";"+c.getSqnCommand());
if(tmp.getSqnCommand()==c.getSqnCommand()){
m[num_ack]=tmp;
num_ack++;
}
}
}while(tmp!=null);
System.out.println("sono alla fine!");
if(isWriteOperation&&num_ack>=WRITE_QUORUM)
return m;
if(!isWriteOperation&&num_ack>=READ_QUORUM)
return m;
return null;
}
#Override
public boolean containsKey(int key){
/*ResponceMessage[] m;
m=sendNewCommand(true,R_CONTAINS_KEY,key);
if(m!=null)
return (boolean)m[0].getResponce();
else
return false;*/
return item_map.containsKey(key);
}
#Override
public boolean removeKey(int key){
//ResponceMessage[] m;
//m=sendNewCommand(true,W_REMOVE_KEY,key);
item_map.remove(key);
return true;
/*if(m!=null)
return (boolean)m[0].getResponce();
else
return false;*/
}
#Override
public boolean put(Product b){
ResponceMessage[] m;
m=sendNewCommand(true,W_PUT,b);
item_map.put(Integer.parseInt(b.getId()),b);
last_key_inserted=Integer.parseInt(b.getId());
if(m!=null){
//last_key_inserted=Integer.parseInt(b.getId());
//return (boolean)m[0].getResponce();
if((boolean)m[0].getResponce())
System.out.println("V_TRUE");
else
System.out.println("FALSE");
}//else
// return false;
return true;
}
#Override
public Product get(int key){
//ResponceMessage[] m;
//m=sendNewCommand(true,R_GET,key);
return item_map.get(key);
/*if(m!=null)
return (Product)m[0].getResponce();
else
return null;*/
}
#Override
public int getLastKeyInserted(){
return last_key_inserted;
}
#Override
public void updatePrice(){
//ResponceMessage[] m;
//m=sendNewCommand(true,R_UPDATE_PRICE,null);
for (Map.Entry pairs : item_map.entrySet()) {
int key=(int)pairs.getKey();
Product p=(Product)pairs.getValue();
double price=p.getInitialPrice();
item_map.remove(key);
if(price>=5)
p.setInitialPrice(price-5);
else
p.setInitialPrice(0);
item_map.put(key, p);
}
}
#Override
public String generateJSONFromMap(){
//ResponceMessage[] m;
//m=sendNewCommand(true,R_GENERATE_JSON_FROM_MAP,null);
String json="[";
for (Map.Entry pairs : item_map.entrySet()) {
Product p=(Product)pairs.getValue();
json+=p.toString();
json+=",";
}
if(json.endsWith(","))
json = json.substring(0,json.length() - 1);
json+="]";
return json;
/* if(m!=null)
return (String)m[0].getResponce();
else
return null;*/
}
private void sendJMSMessageToTopicRepliche(String messageData) {
context.createProducer().send(topicRepliche, messageData);
}
private void sendJMSMessageToTopicRepliche(ObjectMessage messageData) {
context.createProducer().send(topicRepliche, messageData);
}
public AuctionFrontEnd(){
final ConcurrentHashMap<Long,ResponceMessage> responce_list=new ConcurrentHashMap<>();
this.listOfHashMap = new ArrayList<>();
for(int i=0;i<2*NUM_REPLIC;i++){
this.listOfHashMap.add(new ConcurrentHashMap<Long,ResponceMessage>());
}
}
}
The frontEndConsumer class:
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package EJBData;
import Service.Command;
import Service.ResponceMessage;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.jms.ConnectionFactory;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.Queue;
/**
*
* #author melix
*/
#Stateless
public class FrontEndConsumer {
#Resource(lookup = "jms/__defaultConnectionFactory")
private ConnectionFactory connectionFactory;
#Resource(lookup = "QueueFrontEnd")
private Queue queueFrontEnd;
private JMSContext context;
private JMSConsumer consumer;
public void init(){
context = connectionFactory.createContext();
consumer = context.createConsumer(queueFrontEnd);
}
public void cleanQueue(){
while(consumer.receive(1000)!=null){
System.out.println("CANCELLO");
}
}
public ResponceMessage consume(){
Message m = consumer.receive(1000);
if (m != null) {
if(m instanceof ObjectMessage){
try {
Object object = ((ObjectMessage)m).getObject();
ResponceMessage mex=(ResponceMessage)object;
System.out.println("RICEVO");
return (ResponceMessage)mex;
} catch (JMSException ex) {
Logger.getLogger(FrontEndConsumer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
return null;
}
}
The onMessage() method shouldn't need to wait for doOperation() to finish. The JMS API enables asynchronous communication and onMessage() is called on the JMS provider's thread.
Here's two ways of doing string substitution:
name = "Tshepang"
"my name is {}".format(name)
"my name is " + name
How do I do something similar to the first method, using Java?
name = "PaĆlo";
MessageFormat f = new MessageFormat("my name is {0}");
f.format(new Object[]{name});
Or shorter:
MessageFormat.format("my name is {0}", name);
String s = String.format("something %s","name");
Underscore-java has a format() static method. Live example
import com.github.underscore.Underscore;
public class Main {
public static void main(String[] args) {
String name = "Tshepang";
String formatted = Underscore.format("my name is {}", name);
// my name is Tshepang
}
}
You can try this
package template.fstyle;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static java.util.Objects.nonNull;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
#Slf4j
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FStyleFinal {
private static final String PLACEHOLDERS_KEY = "placeholders";
private static final String VARIABLE_NAMES_KEY = "variableNames";
private static final String PLACEHOLDER_PREFIX = "{{";
private static final String PLACEHOLDER_SUFFIX = "}}";
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{\\s*([\\S]+)\\s*}}");
private static Map<String, List<String>> toPlaceholdersAndVariableNames(String rawTemplate) {
List<String> placeholders = newArrayList();
List<String> variableNames = newArrayList();
Matcher matcher = PLACEHOLDER_PATTERN.matcher(rawTemplate);
while (matcher.find()) {
for (int j = 0; j <= matcher.groupCount(); j++) {
String s = matcher.group(j);
if (StringUtils.startsWith(s, PLACEHOLDER_PREFIX) && StringUtils.endsWith(s, PLACEHOLDER_SUFFIX)) {
placeholders.add(s);
} else if (!StringUtils.startsWith(s, PLACEHOLDER_PREFIX) && !StringUtils.endsWith(s, PLACEHOLDER_SUFFIX)) {
variableNames.add(s);
}
}
}
checkArgument(CollectionUtils.size(placeholders) == CollectionUtils.size(variableNames), "template engine error");
Map<String, List<String>> map = newHashMap();
map.put(PLACEHOLDERS_KEY, placeholders);
map.put(VARIABLE_NAMES_KEY, variableNames);
return map;
}
private static String toJavaTemplate(String rawTemplate, List<String> placeholders) {
String javaTemplate = rawTemplate;
for (String placeholder : placeholders) {
javaTemplate = StringUtils.replaceOnce(javaTemplate, placeholder, "%s");
}
return javaTemplate;
}
private static Object[] toJavaTemplateRenderValues(Map<String, String> context, List<String> variableNames, boolean allowNull) {
return variableNames.stream().map(name -> {
String value = context.get(name);
if (!allowNull) {
checkArgument(nonNull(value), name + " should not be null");
}
return value;
}).toArray();
}
private static Map<String, String> fromBeanToMap(Object bean, List<String> variableNames) {
return variableNames.stream().distinct().map(name -> {
String value = null;
try {
value = BeanUtils.getProperty(bean, name);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
log.debug("fromBeanToMap error", e);
}
return Pair.of(name, value);
}).filter(p -> nonNull(p.getRight())).collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
}
public static String render(String rawTemplate, Map<String, String> context, boolean allowNull) {
Map<String, List<String>> templateMeta = toPlaceholdersAndVariableNames(rawTemplate);
List<String> placeholders = templateMeta.get(PLACEHOLDERS_KEY);
List<String> variableNames = templateMeta.get(VARIABLE_NAMES_KEY);
// transform template to java style template
String javaTemplate = toJavaTemplate(rawTemplate, placeholders);
Object[] renderValues = toJavaTemplateRenderValues(context, variableNames, allowNull);
return String.format(javaTemplate, renderValues);
}
public static String render(String rawTemplate, Object bean, boolean allowNull) {
Map<String, List<String>> templateMeta = toPlaceholdersAndVariableNames(rawTemplate);
List<String> variableNames = templateMeta.get(VARIABLE_NAMES_KEY);
Map<String, String> mapContext = fromBeanToMap(bean, variableNames);
return render(rawTemplate, mapContext, allowNull);
}
public static void main(String[] args) {
String template = "hello, my name is {{ name }}, and I am {{age}} years old, a null value {{ not_exists }}";
Map<String, String> context = newHashMap();
context.put("name", "felix");
context.put("age", "18");
String s = render(template, context, true);
log.info("{}", s);
try {
render(template, context, false);
} catch (IllegalArgumentException e) {
log.error("error", e);
}
}
}
Sample output:
[main] INFO template.fstyle.FStyleFinal - hello, my name is felix, and I am 18 years old, a null value null
[main] ERROR template.fstyle.FStyleFinal - error
java.lang.IllegalArgumentException: not_exists should not be null
at com.google.common.base.Preconditions.checkArgument(Preconditions.java:142)
at template.fstyle.FStyleFinal.lambda$toJavaTemplateRenderValues$0(FStyleFinal.java:69)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:545)
at java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
at java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:438)
at java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:444)
at template.fstyle.FStyleFinal.toJavaTemplateRenderValues(FStyleFinal.java:72)
at template.fstyle.FStyleFinal.render(FStyleFinal.java:93)
at template.fstyle.FStyleFinal.main(FStyleFinal.java:113)