JMS-Queues in einem Docker-Container

Während der Arbeit in verschiedenen Projekten ist es immer wieder notwendig, einfache technische Lösungen lokal zu testen, um diese dann in abgewandelter Form für Kunden verwenden zu können. In diesem Fall war es so, dass eine Testumgebung erstellt werden sollte, in der ein JEE-Applikationsserver (Wildfly, Version 10) innerhalb eines Docker-Containers betrieben werden sollte. Neben einfachen Http-Aufrufen der Web-Schnittstelle sollten auch JMS-Queues abgefragt werden können. Insgesamt war folgendes gefordert.

Die Aufgabenstellung

  • Einfache JEE-Anwendung, die eine REST-Schnittstelle für die Annahme von einfachen Texten implementiert und diese als Textnachricht in eine JMS-Queue legt.
  • Einfacher JMS-Consumer, der außerhalb des Docker-Containers die Textnachrichten aus der JMS-Queue abruft.
  • Docker-Container für den Wildfly-Applikationsserver

Neben der oben genannten Aufgabenstellung soll die Management-Ansicht des Wildflys mit Hilfe eines Webbrowsers zugreifbar sein. Der Aufruf der REST-Schnittstelle soll mit Hilfe eines Plugins innerhalb des Webbrowsers erfolgen.

REST-Schnittstelle

Die JEE-Anwendung für die Annahme der REST-Aufrufe und die Weiterleitung der Textnachrichten in die JMS-Queue ist relativ simpel gehalten und verwendet Annotationen, um die benötigten Ressourcen anzufordern:

@Stateless
@Path("/message")
public class RestApi {
  private final static Logger LOG = LoggerFactory.getLogger(RestApi.class);

  @Resource(lookup = "java:/jms/queue/testQueue")
  private Queue testQueue;
  
  @Inject
  private JMSContext context; 
  
  @POST
  @Consumes(MediaType.TEXT_PLAIN)
  public Response createMessage(String messageText) {
    try {
      LOG.info("Received message: '" + messageText + "'");
      
      context.createProducer().send(testQueue, messageText);
      
      LOG.info("Sent jms message: '" + messageText + "'");
      return Response.ok().build();
    } catch (Exception e) {
      throw new BadRequestException(e);
    }
  }
}

JMS-Client

Um die mittels REST-Schnittstelle in die JMS-Queue geschriebenen Nachrichten außerhalb des Docker-Containers abzurufen, wurde der folgende einfache JMS-Client erstellt. Zuerst wird eine JNDI-Verbindung aufgebaut und die RemoteConnectionFactory und die Destination, also die JMS-Queue abgerufen. Anschließend wird eine Verbindung zu dieser Queue erstellt und die Nachrichten abgerufen, bis der Benutzer dies durch die Betätigung der Taste q beendet.

public class JmsExampleConsumer {

  private final static String CONNECTION_FACTORY = "jms/RemoteConnectionFactory";
  private final static String QUEUE_NAME = "java:/jms/queue/testQueue";
  private static final String WILDFLY_URL = "http-remoting://localhost:8080";
  private static final String WILDFLY_USER = "jboss";
  private static final String WILDFLY_PASSWORD = "jboss";
  private Context jndiContext = null;
  private JMSContext jmsContext = null;

  public static void main(String[] args) {
    JmsExampleConsumer consumer = new JmsExampleConsumer();

    consumer.receiveMessages();
  }

  private void receiveMessages() {
    System.out.println("Press q to exit...");
    try {
      final Properties env = new Properties();
      env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory");
      env.put(Context.PROVIDER_URL, WILDFLY_URL);
      env.put(Context.SECURITY_PRINCIPAL, WILDFLY_USER);
      env.put(Context.SECURITY_CREDENTIALS, WILDFLY_PASSWORD);

      jndiContext = new InitialContext(env);

      ConnectionFactory cf = (ConnectionFactory) jndiContext.lookup(CONNECTION_FACTORY);
      System.out.println("Found Connection Factory...");
      Destination queue = (Destination) jndiContext.lookup(QUEUE_NAME);
      System.out.println("Found Queue...");

      jmsContext = cf.createContext(WILDFLY_USER, WILDFLY_PASSWORD);
      System.out.println("Created jms context...");
      TextMessage message = null;
      JMSConsumer consumer = jmsContext.createConsumer(queue);
      System.out.println("Connected to queue...");
      while (System.in.read() != 'q') {
        message = (TextMessage) consumer.receive(1000);
        if (message != null) {
          System.out.println("Received message: '" + message.getText() + "'");
        }
      }

    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (jmsContext != null) {
        jmsContext.close();
      }

      if (jndiContext != null) {
        try {
          jndiContext.close();
        } catch (NamingException e) {
          e.printStackTrace();
        }
      }
    }
  }
}

Docker-Container

Im Dockerfile wird das Wildfly-Image für die Wildfly-Version 10.1.0 von DockerHub verwendet und die Ports für die Management-Console und für HTTP-Verbindungen nach außen freigegeben. Anschließend werden ein Management-User für den Zugriff auf die Management-Console sowie das Deployment und ein Application-User für den Zugriff auf die JNDI- und JMS-Queue erstellt. Zum Schluss wird der Wildfly mit der Konfiguration standalone-full gestartet.

From jboss/wildfly:10.1.0.Final

EXPOSE 8080 9990

RUN /opt/jboss/wildfly/bin/add-user.sh jboss jboss --silent

CMD /opt/jboss/wildfly/bin/standalone.sh -c standalone-full.xml -b0.0.0.0 -bmanagement=0.0.0.0

Das Problem

Leider hat der so erstellte Docker-Container das Problem, dass es mit dem Client nicht möglich ist, die Verbindung zur JMS-Queue herzustellen. Grund hierfür ist, dass der Wildfly bei Verwendung der IP-Adresse 0.0.0.0 für den HTTP-Connector in der Remote-Client-Implementierung den Hostnamen des Containers in den abgefragten Verbindungsdaten zurückliefert. Da dieser Hostname aber nicht außerhalb des Docker-Containers sichtbar ist, kann keine Verbindung aufgebaut werden.

Ein weiteres Problem ist, dass für den Aufbau einer JNDI-Verbindung ein Application-User eingerichtet sein muss. Dieser muß zusätzlich auch noch das Recht besitzen, sich mit einer Queue verbinden zu dürfen.

Die Lösung

Die oben genannten Probleme haben schließlich zu dem folgenden Ansatz geführt:

From jboss/wildfly:10.1.0.Final

EXPOSE 8080 9990

RUN /opt/jboss/wildfly/bin/add-user.sh jboss jboss --silent
RUN /opt/jboss/wildfly/bin/add-user.sh -a jboss jboss -g guest --silent

CMD /opt/jboss/wildfly/bin/standalone.sh -c standalone-full.xml -b=$(hostname -i) -bmanagement=0.0.0.0

Für die Erstellung eines Application-Users wurde eine weitere Zeile innerhalb des Dockerfiles eingefügt. Diese erstellt den Benutzernamen und ordnet ihn der Gruppe guest zu, der in der verwendeten Konfiguration die benötigten Rechte für den Zugriff auf die JMS-Queue zugeordnet sind.

Das Problem des Zugriffs wurde dadurch gelöst, dass für den HTTP-Connector die konkrete IP-Adresse des Docker-Containers beim Start übergeben wird, die es auch von außerhalb ermöglicht, darauf zuzugreifen.

Schlussbemerkung

Der gesamte Quellcode ist in der Form eines Maven-Projektes auf GitHub unter https://github.com/conciso/jms-with-docker verfügbar und kann dort eingesehen bzw. abgerufen werden.

Referenzen