Java EE, JCA, and jNode 2.X announce

  • Tutorial

Good day,% username%.
I’ll say right away, 99% of this post is about Java EE Connector Architecture, with code examples. Where did you get 1% about Fidonet you will understand at the very end.

Resume for the lazy
JMS and JCA - sibling inbox accepts MessageDrivenBean; outgoing senders are sent through ConnectionFactory.
The minimum package for an inbound connection is 4 classes, for an outbound connection - 8 classes and the adapter is configured on the application server side.
Further - only details and pain


For a start - the history of the issue and the solution to the business problem.

Formulation of the problem


I was given the task of integrating an existing business system (“System A”) with another system that was developed many years ago and only understands its own data transfer protocol (“System B”). It is impossible to modify other people's systems; accordingly, the task boiled down to writing a certain bus / proxy. Integration consists in transferring messages back and forth with their conversion in the process from one format to another.

System "A" had many modern integration mechanisms, the easiest to use were recognized as web services. A standard integration skeleton for JEE - JAX-WS + EJB + JMS for guaranteed message delivery was promptly filed for this case.
But there were no standard tools for working with system “B”. Timid attempts to work with the network from the EJB context were unsuccessful, Google suggested two solutions to the problem: crutch servlets for working with non-http or write a JCA adapter. It is clear that the second path was chosen - I had not worked with JCA before, and it is always interesting to learn something new.

Study


Starting to dig Google, I broke off quite a bit. Everywhere they wrote WHAT exactly needs to be done (connector, manager, adapter, etc.), but almost never wrote how to do it. The standard way to “look at someone else’s code and understand the process” failed - someone else’s code was so meager that it was not possible to understand something.

Two things saved me: JSR 322 and the only google adapter on google code . Actually, this was the starting point - having deployed examples from jca-sockets and opening pdf, I began to understand and understand with a poke of how it actually works.

Having spent about 16 hours researching and experimenting, I found out the following:

The JCA module has two independent parts inside it: Inbox and Outbox. These parts can be both together and separately. Moreover, there may be several. The module itself is registered with a class that implements javax.resource.spi.ResourceAdapter and specified in META-INF / ra.xml , while the ResourceAdapter is needed primarily for working with Inboxes; For Outgoing, the adapter does nothing and its skeleton can even not be filled.

Inbox


The incoming channel binds to MessageEndpoint 'y (usually it is @MessageDrivenBean ; yes, JCA is the guts of JMS) and is activated by ActivationSpec ' ohm.
META-INF / ra.xml - Description of ResourceAdapter and inbound streams
ra.xml
xxx-servicesFidoNet2.5in.fidonode.binkp.ra.BinkpServerResourceAdapterversionjava.lang.Stringjnode-jee 2.5 binkp/1.1in.fidonode.binkp.ra.BinkpMessageListenerin.fidonode.binkp.ra.BinkpActivationSpeclistenPortlistenPortjava.lang.Integer24554



The BinkpMessageListener interface is for clients and should be in the classpath;

I will bring it here:
public interface BinkpMessageListener {
	public void onMessage(FidoMessage message);
}


Now consider the simplest implementation of the ResourceAdapter.
BinkpServerResourceAdapter.java
public class BinkpServerResourceAdapter implements ResourceAdapter, Serializable {
	private static final long serialVersionUID = 1L;
	private static Logger log = Logger.getLogger(BinkpServerResourceAdapter.class
			.getName());
	private ConcurrentHashMap activationMap = 
			new ConcurrentHashMap();
	private BootstrapContext ctx;
	private String version;
	@Override
	public void endpointActivation(MessageEndpointFactory endpointFactory,
			ActivationSpec spec) throws ResourceException {
		BinkpEndpoint activation = new BinkpEndpoint(ctx.getWorkManager(),
				(BinkpActivationSpec) spec, endpointFactory);
		activationMap.put((BinkpActivationSpec) spec, activation);
		activation.start();
		log.info("endpointActivation(" + activation + ")");
	}
	@Override
	public void endpointDeactivation(MessageEndpointFactory endpointFactory,
			ActivationSpec spec) {
		BinkpEndpoint activation = activationMap.remove(spec);
		if (activation != null)
			activation.stop();
		log.info("endpointDeactivation(" + activation + ")");
	}
	@Override
	public void start(BootstrapContext ctx)
			throws ResourceAdapterInternalException {
		this.ctx = ctx;
		log.info("start()");
	}
	@Override
	public void stop() {
		for (BinkpEndpoint act : activationMap.values()) {
			act.stop();
		}
		activationMap.clear();
		log.info("stop()");
	}
	@Override
	public XAResource[] getXAResources(ActivationSpec[] arg0)
			throws ResourceException {
		return null;
	}
	public String getVersion() {
		return version;
	}
	public void setVersion(String version) {
		this.version = version;
	}
}



What's going on here? When loading the JCA module, an instance of the BinkpServerResourceAdapter class is created, its parameters are filled in (in this case, the version field) and the start () method is called.
In fact, you can do a lot of things inside the start () method, but in this example we just save the context for getting WorkManager from it later .

When the application server finds @MessageDrivenBean , it tries to find an adapter that sends messages to the interface that implements the bean. For JMS, this is a MessageListener , we have a BinkpMessageListener . The ActivationSpec is created (we have BinkpActivationSpec, which implements javax.resource.spi.ActivationSpec), the fields in which are filled in according to the data in activationConfig, a MessageEndpointFactory is created and ResourceAdapter.endpointActivation () is called. In this function, you need to create the “server” that will accept incoming connections, whether it be a tcp / ip server or a stream for working with unix-socket, to create on the basis of the config that was in MDB. The BinkpEndpoint class is this very “server”.
Binkpendnd.java
public class BinkpEndpoint implements Work, FidoMessageListener {
	private static final Logger logger = Logger.getLogger(BinkpEndpoint.class
			.getName());
	private BinkpServer server;
	private final WorkManager workManager;
	private final MessageEndpointFactory messageEndpointFactory;
	public BinkpEndpoint(WorkManager workManager,
			BinkpActivationSpec activationSpec,
			MessageEndpointFactory messageEndpointFactory) {
		this.workManager = workManager;
		this.messageEndpointFactory = messageEndpointFactory;
		server = new BinkpServer(activationSpec.getListenPort(), this);
	}
	public void start() throws ResourceException {
		workManager.scheduleWork(this);
	}
	public void stop() {
			if (server != null) {
				server.stop();
			}
	}
	/** из FidoMessageListener **/
	@Override
	public Message incomingMessage(FidoMessage message) {
			String message = msg.encode();
			BinkpMessageListener listener = (BinkpMessageListener) messageEndpointFactory
					.createEndpoint(null);
			listener.onMessage(message);
	}
	/** из Work **/
	@Override
	public void run() {
		server.start();
	}
	/** из Work **/
	@Override
	public void release() {
		stop();
	}
}



You may notice that some endpoints appear everywhere. I had some gag with this, so I’ll decrypt:
Endpoint is what the “incoming” stream is listening to. It is to him that the endpointActication
MessageEndpoint functions - the MDB instance that processes a particular message. Obtained by calling MessageEndpointFactory.createEndpoint () (This function cannot be called from the main thread). It easily casts to the MDB interface.

Actually, that's all. I’ll omit the implementation of BinkpServer, but the principle should be clear, the minimum “Inbound” JCA is made of four classes (ResourceAdapter, MessageListener, ActivationSpec, Endpoint)

Creating an Endpoint and processing the incoming:
@MessageDriven(messageListenerInterface = BinkpMessageListener.class, 
activationConfig = { @ActivationConfigProperty(propertyName = "listenPort", propertyValue = "24554") })
public class ReceiveMessageBean implements BinkpMessageListener {
	@Override
	public void onMessage(FidoMessage msg) {
		// do smth with mesaage
	}
}


Outgoing



And here - everything is more fun, the minimum “Outgoing” JCA is made from as many as 8 classes, which is 2 times more than the “Incoming”. But let's take it in order.

META-INF / ra.xml - description of ResourceAdapter and outbound streams

ra.xml
xxx-servicesFidoNet2.5in.fidonode.binkp.ra.BinkpServerResourceAdapterversionjava.lang.Stringjnode-jee 2.5 binkp/1.1in.fidonode.binkp.ra.ManagedConnectionFactoryin.fidonode.binkp.ra.ConnectionFactoryin.fidonode.binkp.ra.ConnectionFactoryImplin.fidonode.binkp.ra.Connectionin.fidonode.binkp.ra.ConnectionImplNoTransactionfalse



The Connection and ConnectionFactory interfaces are for clients and should be in the classpath. Immediately bring them here, there is nothing interesting. I will not give

BinkpClient :-)
public interface Connection {
	public BinkpClient connect(String hostname, int port);
}
public interface ConnectionFactory {
	public Connection createConnection();
}


Connections are Managed and Unmanaged. The first - with whistles, listeners and others, the second - without.
A class that implements ManagedConnectionFactory must be able to create both types of connections.
ManagedConnectionFactory.java
public class ManagedConnectionFactory implements
		javax.resource.spi.ManagedConnectionFactory {
	private PrintWriter logwriter;
	private static final long serialVersionUID = 1L;
	/**
	 * Создание фабрики для unmanaged-соединений
	 */
	@Override
	public Object createConnectionFactory() throws ResourceException {
		return new ConnectionFactoryImpl();
	}
	/**
	 * Создание managed-фабрики для managed-connection
	 */
	@Override
	public Object createConnectionFactory(ConnectionManager cxManager)
			throws ResourceException {
		return new ManagedConnectionFactoryImpl(this, cxManager);
	}
	/**
	 * Создание managed-соединения
	 */
	@Override
	public ManagedConnection createManagedConnection(Subject subject,
			ConnectionRequestInfo cxRequestInfo) throws ResourceException {
		return new in.fidonode.binkp.ra.ManagedConnection();
	}
	@Override
	public PrintWriter getLogWriter() throws ResourceException {
		return logwriter;
	}
	@SuppressWarnings("rawtypes")
	@Override
	public ManagedConnection matchManagedConnections(Set connectionSet,
			Subject subject, ConnectionRequestInfo cxRequestInfo)
			throws ResourceException {
		ManagedConnection result = null;
		Iterator it = connectionSet.iterator();
		while (result == null && it.hasNext()) {
			ManagedConnection mc = (ManagedConnection) it.next();
			if (mc instanceof in.fidonode.binkp.ra.ManagedConnection) {
				result = mc;
			}
		}
		return result;
	}
	@Override
	public void setLogWriter(PrintWriter out) throws ResourceException {
		logwriter = out;
	}
}



When an application requests a connector from a JEE server, the application server asks the ManagedConnectionFactory to create a ConnectionFactory and gives it to the application.

As you can see, ConnectionFactory is also Managed and Unmanaged. In principle, all this can be reduced to one class, but it greatly depends on what exactly and how we transfer, whether there are transactions, etc.
ConnectionFactoryIml just makes new ConnectionImpl () , but ManagedConnectionFactoryImpl is a little more complicated:

ManagedConnectionFactoryImpl.java
public class ManagedConnectionFactoryImpl implements ConnectionFactory {
	private ManagedConnectionFactory factory;
	private ConnectionManager manager;
	public ManagedConnectionFactoryImpl(ManagedConnectionFactory factory,
			ConnectionManager manager) {
		super();
		this.factory = factory;
		this.manager = manager;
	}
/** создает managed-соединение через родителя-ManagedConnectionFactory **/
	@Override
	public Connection createConnection() {
		try {
			return (Connection) manager.allocateConnection(factory, null);
		} catch (ResourceException e) {
			return null;
		}
	}
}



ManagedConnection that implements javax.resource.spi.ManagedConnection is a wrapper for the Connection interface, which just adds whistles and listeners. It is this class that returns ManagedConnectionFactory.createManagedConnection () , which we call when creating a connection from ManagedConnectionFactoryImpl.createConnection () through ConnectionManager.allocateConnection ()

ManagedConnection.java
public class ManagedConnection implements javax.resource.spi.ManagedConnection {
	private PrintWriter logWriter;
	private Connection connection;
	private List listeners;
	public ManagedConnection() {
		listeners = Collections
				.synchronizedList(new ArrayList());
	}
	@Override
	public void associateConnection(Object connection) throws ResourceException {
		if (connection != null && connection instanceof Connection) {
			this.connection = (Connection) connection;
		}
	}
	@Override
	public Object getConnection(Subject subject,
			ConnectionRequestInfo cxRequestInfo) throws ResourceException {
		if (connection == null) {
			connection = new ManagedConnectionImpl();
		}
		return connection;
	}
	@Override
	public void cleanup() throws ResourceException {
	}
	@Override
	public void destroy() throws ResourceException {
	}
	@Override
	public PrintWriter getLogWriter() throws ResourceException {
		return logWriter;
	}
	@Override
	public ManagedConnectionMetaData getMetaData() throws ResourceException {
		throw new NotSupportedException();
	}
	@Override
	public XAResource getXAResource() throws ResourceException {
		throw new NotSupportedException();
	}
	@Override
	public LocalTransaction getLocalTransaction() throws ResourceException {
		return null;
	}
	@Override
	public void setLogWriter(PrintWriter out) throws ResourceException {
		logWriter = out;
	}
	@Override
	public void addConnectionEventListener(ConnectionEventListener listener) {
		if (listener != null) {
			listeners.add(listener);
		}
	}
	@Override
	public void removeConnectionEventListener(ConnectionEventListener listener) {
		if (listener != null) {
			listeners.remove(listener);
		}
	}
}



Well, now we come to the simplest thing - the implementation of the connection :-)
public class ConnectionImpl implements Connection {
	@Override
	public BinkpClient connect(String hostname, int port) {
		return new BinkpClient(hostname, port);
	}
}


The resulting chain of calls to establish an outgoing connection
ManagedConnectionFactory.createConnectionFactory ()
-> ManagedConnectionFactoryImpl.createConnection ()
-> SonnectionManager.allocateConnection ()
---> ManagedConnectionFactory.createManagedConnection ()
----> ManagedConnection.getConnection ()
---- -> ManagedConnectionImpl.connect ()

Well, do not forget to configure the application server to work with this adapter, well, specify jndi.

Code to call:
	private BinkpClient createBinkpClient(String host, int port) {
		ConnectionFactory cf = ((ConnectionFactory) new InitialContext().lookup("java:eis/BinkpConnectionFactory"));
		Connection conn = cf.getConnection();
		return conn.connect(host, port);
	}

And where does Fido?



And almost no reason. The fact is that the original task was not about binkp at all, but it was working, which means it fell under the NDA. Therefore, after understanding JCA and deciding what you need to write an article on Habré (by the way, looking back, I begin to understand why no one has written such an article. And this is without transactions!), I unfreeze the old idea - fork jnode for JEE servers, to run the nodes as a single ear. At one time, it was JCA's knowledge that was not enough for me to start the project :-)

For this, I wrote all the above examples, and they even worked. So if you want to practice java ee in general and refactoring from java se in particular - write letters and commit code. Yes, I still take points.

Thank you for your attention, stay with us. Typos can be written in the comments, I have no doubt that there are dozens of them.

Also popular now: