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 { ListsqlStatements = 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