How to Emulate a remote JVM in JUnit using Spring, Ant, H2 and ActiveMQ
recently i needed to test a scenario where one JVM was communicating to another using both a shared database and JMS. i also needed to have each JVM be "invisible" to each other and make sure that the couldn't 'accidentally' share things across and application context.
so, to do this, i needed to kick start a separate JVM from inside a JUnit test, and communicate via JMS and a database... fun.
here's what i needed;
- shared database (standalone server)
- shared JMS (standalone server)
- JVM A - application context with unique configuration
- JVM B - application context with unique configuration
so, here's the solution i put together with some google/stackoverflow help and the following;
- Spring
- JUnit 4
- Ant
- H2
- ActiveMQ
here's the infrastructure utilities class. it's responsible for setting up 'shared' infrastructure such as H2 and ActiveMQ. it also provides hooks for binding to JNDI in a standalone context to help with sharing the resources in an 'agnostic' kind of way.
package de.incompleteco.spring.batch.ha;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.spring.ActiveMQConnectionFactory;
import org.h2.jdbcx.JdbcConnectionPool;
import org.h2.jdbcx.JdbcDataSource;
import org.h2.tools.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
public class InfrastructureUtils {
private static final Logger logger = LoggerFactory.getLogger(InfrastructureUtils.class);
private static Server server;
private static BrokerService broker;
public static void startH2() throws Exception {
if (server == null) {
server = Server.createTcpServer("-tcpAllowOthers");
server.start();
logger.info(server.getStatus());
}//end if
}
public static void stopH2() throws Exception {
if (server != null) {
server.stop();
}//end if
}
public static void startAMQ() throws Exception {
if (broker == null) {
broker = new BrokerService();
broker.addConnector("tcp://localhost:61616");
broker.setUseJmx(false);
broker.setUseShutdownHook(true);
broker.start();
broker.deleteAllMessages();//clean up
}//end if
}
public static void stopAMQ() throws Exception {
if (broker != null) {
broker.stop();
}//end if
}
public static void bindLocalAMQ(String connectionFactoryName,String... queueNames) {
//get jndi
SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.getCurrentContextBuilder();
//get the connection
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
connectionFactory.setBrokerURL("tcp://localhost:61616");
//bind
builder.bind("jms/" + connectionFactoryName,connectionFactory);
//setup the queues
for (String name : queueNames) {
builder.bind("jms/" + name,new ActiveMQQueue(name));
}//end for
}
public static DataSource bindLocalH2(String dataSourceName) throws Exception {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:tcp://localhost/~/test");
//build a pool and bind
JdbcConnectionPool pool = JdbcConnectionPool.create(dataSource);
SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.getCurrentContextBuilder();
builder.bind("jdbc/" + dataSourceName, pool);
//return it in case there's other uses
return dataSource;
}
public static String[] convertSqlFile(String location) throws Exception {
List sqlStatements = new ArrayList();
//load up the file
BufferedReader reader = new BufferedReader(new InputStreamReader(TestSimpleBatchHAService.class.getResourceAsStream(location)));
String line;
StringBuilder statement = new StringBuilder();
while ((line = reader.readLine()) != null) {
if (line.contains(";") && !line.contains("--")) {
statement.append(line);
sqlStatements.add(statement.toString().replace(';',' '));
statement = new StringBuilder();//reset the string
} else if (line.contains("--")) {
//ignore
} else {
statement.append(line);
}//end if
}//end while
reader.close();
//return
return sqlStatements.toArray(new String[sqlStatements.size()]);
}
}
here's the RemoteJVMEmulator class. it's a 'standalone' class (static void main(..)) that bootstraps and application context. it also uses the infrastructure utilities to bind the resources to the JNDI.
package de.incompleteco.spring.batch.ha;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
public class RemoteJVMEmulator {
private static final Logger logger = LoggerFactory.getLogger(RemoteJVMEmulator.class);
//bind shared resources to JNDI
public static void setupJNDI() throws Exception {
SimpleNamingContextBuilder.emptyActivatedContextBuilder();
//bind AMQ
InfrastructureUtils.bindLocalAMQ("ConnectionFactory","request.queue","reply.queue");
//bind h2
InfrastructureUtils.bindLocalH2("DataSource");
}
public static void main(String[] args) throws Exception {
//setup jndi
setupJNDI();
//start the app context
ApplicationContext context = new ClassPathXmlApplicationContext(args);
//print a statement saying "it's up"
logger.info(RemoteJVMEmulator.class.getSimpleName() + " is up " + context.getStartupDate());
}
}
and here's the test runner. this uses the infrastructure utilities to startup the shared services and bind them locally. it then uses and inner class to build and run and Ant task that will kick off the RemoteJVMEmulator class. a couple of things to note about it though;
- the Ant task is started in a separate thread - mainly because it blocks
- the Ant task is started first to act as the 'remote client'/'listener'
- the 'remote client'/'listener' needs to be setup such that you don't need to be in the same JVM to access any information from it. (treat it as a 'remote' server deployed and running without any interaction services - you could connect to it via JMX or expose other services, that's up to you)
package de.incompleteco.spring.batch.ha;
import static org.junit.Assert.assertFalse;
import java.io.PrintStream;
import javax.sql.DataSource;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.DemuxOutputStream;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.Path;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
//don't run this test in CI
@Ignore
public class TestSimpleBatchHAService {
@BeforeClass
public static void beforeClass() throws Exception {
//start a builder
SimpleNamingContextBuilder.emptyActivatedContextBuilder();
//setup the services (h2 and amq)
InfrastructureUtils.startH2();
//setup the database
DataSource dataSource = InfrastructureUtils.bindLocalH2("DataSource");
setupH2Data(dataSource);
//start amq
InfrastructureUtils.startAMQ();
//bind
InfrastructureUtils.bindLocalAMQ("ConnectionFactory","request.queue","reply.queue");
}
@AfterClass
public static void afterClass() throws Exception {
//shutdown the services
InfrastructureUtils.stopH2();
InfrastructureUtils.stopAMQ();
}
private static void setupH2Data(DataSource dataSource) throws Exception {
//execute the statements
JdbcTemplate template = new JdbcTemplate(dataSource);
//drop statements
String[] statements = InfrastructureUtils.convertSqlFile("/org/springframework/batch/core/schema-drop-h2.sql");
try {
for (String statement : statements) {
template.execute(statement);
}//end for
}
catch (Exception e) { }
statements = InfrastructureUtils.convertSqlFile("/META-INF/sql/schema-ext-drop-h2.sql");
try {
for (String statement : statements) {
template.execute(statement);
}//end for
}
catch (Exception e) { }
//create statements
statements = InfrastructureUtils.convertSqlFile("/org/springframework/batch/core/schema-h2.sql");
for (String statement : statements) {
template.execute(statement);
}//end for
//create statements
statements = InfrastructureUtils.convertSqlFile("/META-INF/sql/schema-ext-h2.sql");
for (String statement : statements) {
template.execute(statement);
}//end for
}
@Test
public void testExecute() throws Exception {
//start a thread to run the remote
new Thread(new RemoteJVMRunner()).start();
//now start the 'server'
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:/META-INF/spring/server-context.xml");
//now that it's started, run the job
Job job = context.getBean(Job.class);
JobParameters parameters = new JobParametersBuilder().addLong("runtime",System.currentTimeMillis()).toJobParameters();
JobLauncher launcher = context.getBean("remoteJobLauncher", JobLauncher.class);
JobExecution execution = launcher.run(job,parameters);
JobExplorer explorer = context.getBean(JobExplorer.class);
//monitor
while (explorer.getJobExecution(execution.getId()).isRunning()) {
Thread.sleep(500);
}//end while
//reload the execution
execution = explorer.getJobExecution(execution.getId());
//check
assertFalse(execution.getStatus().isUnsuccessful());
}
class RemoteJVMRunner implements Runnable {
@Override
public void run() {
Project project = new Project();
project.setName("remote-jvm");
project.init();
//setup the logger
DefaultLogger logger = new DefaultLogger();
project.addBuildListener(logger);
logger.setOutputPrintStream(System.out);
logger.setErrorPrintStream(System.err);
logger.setMessageOutputLevel(Project.MSG_INFO);
System.setOut(new PrintStream(new DemuxOutputStream(project,false)));
System.setErr(new PrintStream(new DemuxOutputStream(project,true)));
//start the project
project.fireBuildStarted();
Java java = new Java();
java.setProject(project);;
java.setTaskName("run-remote-jvm");
java.setFork(true);
java.setFailonerror(true);
//set the classname
java.setClassname(RemoteJVMEmulator.class.getName());
java.setClasspath(new Path(project,System.getProperty("java.class.path")));
//create arguments
java.createArg().setValue("classpath:/META-INF/spring/client-context.xml");
//init
java.init();
//execute
java.executeJava();
}
}
}
No comments:
Post a Comment