001/* 002 * Copyright 2006-2013 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.springframework.batch.core.launch.support; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.List; 023 024import org.apache.commons.logging.Log; 025import org.apache.commons.logging.LogFactory; 026import org.springframework.batch.core.Job; 027import org.springframework.batch.core.configuration.DuplicateJobException; 028import org.springframework.batch.core.configuration.JobFactory; 029import org.springframework.batch.core.configuration.JobRegistry; 030import org.springframework.batch.core.configuration.support.DefaultJobLoader; 031import org.springframework.batch.core.configuration.support.GenericApplicationContextFactory; 032import org.springframework.batch.core.configuration.support.JobLoader; 033import org.springframework.batch.core.launch.JobLauncher; 034import org.springframework.beans.factory.BeanFactory; 035import org.springframework.beans.factory.config.AutowireCapableBeanFactory; 036import org.springframework.context.ApplicationContext; 037import org.springframework.context.support.ClassPathXmlApplicationContext; 038import org.springframework.core.io.Resource; 039import org.springframework.util.Assert; 040 041/** 042 * <p> 043 * Command line launcher for registering jobs with a {@link JobRegistry}. 044 * Normally this will be used in conjunction with an external trigger for the 045 * jobs registered, e.g. a JMX MBean wrapper for a {@link JobLauncher}, or a 046 * Quartz trigger. 047 * </p> 048 * 049 * <p> 050 * With any launch of a batch job within Spring Batch, a Spring context 051 * containing the {@link Job} has to be created. Using this launcher, the jobs 052 * are all registered with a {@link JobRegistry} defined in a parent application 053 * context. The jobs are then set up in child contexts. All dependencies of the 054 * runner will then be satisfied by autowiring by type from the parent 055 * application context. Default values are provided for all fields except the 056 * {@link JobRegistry}. Therefore, if autowiring fails to set it then an 057 * exception will be thrown. 058 * </p> 059 * 060 * @author Dave Syer 061 * 062 */ 063public class JobRegistryBackgroundJobRunner { 064 065 /** 066 * System property key that switches the runner to "embedded" mode 067 * (returning immediately from the main method). Useful for testing 068 * purposes. 069 */ 070 public static final String EMBEDDED = JobRegistryBackgroundJobRunner.class.getSimpleName() + ".EMBEDDED"; 071 072 private static Log logger = LogFactory.getLog(JobRegistryBackgroundJobRunner.class); 073 074 private JobLoader jobLoader; 075 076 private ApplicationContext parentContext = null; 077 078 public static boolean testing = false; 079 080 final private String parentContextPath; 081 082 private JobRegistry jobRegistry; 083 084 private static List<Exception> errors = Collections.synchronizedList(new ArrayList<Exception>()); 085 086 /** 087 * @param parentContextPath the parentContextPath to be used by the JobRegistryBackgroundJobRunner. 088 */ 089 public JobRegistryBackgroundJobRunner(String parentContextPath) { 090 super(); 091 this.parentContextPath = parentContextPath; 092 } 093 094 /** 095 * A loader for the jobs that are going to be registered. 096 * 097 * @param jobLoader the {@link JobLoader} to set 098 */ 099 public void setJobLoader(JobLoader jobLoader) { 100 this.jobLoader = jobLoader; 101 } 102 103 /** 104 * A job registry that can be used to create a job loader (if none is provided). 105 * 106 * @param jobRegistry the {@link JobRegistry} to set 107 */ 108 public void setJobRegistry(JobRegistry jobRegistry) { 109 this.jobRegistry = jobRegistry; 110 } 111 112 /** 113 * Public getter for the startup errors encountered during parent context 114 * creation. 115 * @return the errors 116 */ 117 public static List<Exception> getErrors() { 118 synchronized (errors) { 119 return new ArrayList<Exception>(errors); 120 } 121 } 122 123 private void register(String[] paths) throws DuplicateJobException, IOException { 124 125 maybeCreateJobLoader(); 126 127 for (int i = 0; i < paths.length; i++) { 128 129 Resource[] resources = parentContext.getResources(paths[i]); 130 131 for (int j = 0; j < resources.length; j++) { 132 133 Resource path = resources[j]; 134 logger.info("Registering Job definitions from " + Arrays.toString(resources)); 135 136 GenericApplicationContextFactory factory = new GenericApplicationContextFactory(path); 137 factory.setApplicationContext(parentContext); 138 jobLoader.load(factory); 139 } 140 141 } 142 143 } 144 145 /** 146 * If there is no {@link JobLoader} then try and create one from existing 147 * bean definitions. 148 */ 149 private void maybeCreateJobLoader() { 150 151 if (jobLoader != null) { 152 return; 153 } 154 155 String[] names = parentContext.getBeanNamesForType(JobLoader.class); 156 if (names.length == 0) { 157 if (parentContext.containsBean("jobLoader")) { 158 jobLoader = parentContext.getBean("jobLoader", JobLoader.class); 159 return; 160 } 161 if (jobRegistry != null) { 162 jobLoader = new DefaultJobLoader(jobRegistry); 163 return; 164 } 165 } 166 167 jobLoader = parentContext.getBean(names[0], JobLoader.class); 168 return; 169 170 } 171 172 /** 173 * Supply a list of application context locations, starting with the parent 174 * context, and followed by the children. The parent must contain a 175 * {@link JobRegistry} and the child contexts are expected to contain 176 * {@link Job} definitions, each of which will be registered wit the 177 * registry. 178 * 179 * Example usage: 180 * 181 * <pre> 182 * $ java -classpath ... JobRegistryBackgroundJobRunner job-registry-context.xml job1.xml job2.xml ... 183 * </pre> 184 * 185 * The child contexts are created only when needed though the 186 * {@link JobFactory} interface (but the XML is validated on startup by 187 * using it to create a {@link BeanFactory} which is then discarded). 188 * 189 * The parent context is created in a separate thread, and the program will 190 * pause for input in an infinite loop until the user hits any key. 191 * 192 * @param args the context locations to use (first one is for parent) 193 * @throws Exception if anything goes wrong with the context creation 194 */ 195 public static void main(String... args) throws Exception { 196 197 Assert.state(args.length >= 1, "At least one argument (the parent context path) must be provided."); 198 199 final JobRegistryBackgroundJobRunner launcher = new JobRegistryBackgroundJobRunner(args[0]); 200 errors.clear(); 201 202 logger.info("Starting job registry in parent context from XML at: [" + args[0] + "]"); 203 204 new Thread(new Runnable() { 205 @Override 206 public void run() { 207 try { 208 launcher.run(); 209 } 210 catch (RuntimeException e) { 211 errors.add(e); 212 throw e; 213 } 214 } 215 }).start(); 216 217 logger.info("Waiting for parent context to start."); 218 while (launcher.parentContext == null && errors.isEmpty()) { 219 Thread.sleep(100L); 220 } 221 222 synchronized (errors) { 223 if (!errors.isEmpty()) { 224 logger.info(errors.size() + " errors detected on startup of parent context. Rethrowing."); 225 throw errors.get(0); 226 } 227 } 228 errors.clear(); 229 230 // Paths to individual job configurations. 231 final String[] paths = new String[args.length - 1]; 232 System.arraycopy(args, 1, paths, 0, paths.length); 233 234 logger.info("Parent context started. Registering jobs from paths: " + Arrays.asList(paths)); 235 launcher.register(paths); 236 237 if (System.getProperty(EMBEDDED) != null) { 238 launcher.destroy(); 239 return; 240 } 241 242 synchronized (JobRegistryBackgroundJobRunner.class) { 243 System.out 244 .println("Started application. Interrupt (CTRL-C) or call JobRegistryBackgroundJobRunner.stop() to exit."); 245 JobRegistryBackgroundJobRunner.class.wait(); 246 } 247 launcher.destroy(); 248 249 } 250 251 /** 252 * Unregister all the {@link Job} instances that were registered by this 253 * post processor. 254 * @see org.springframework.beans.factory.DisposableBean#destroy() 255 */ 256 private void destroy() throws Exception { 257 jobLoader.clear(); 258 } 259 260 private void run() { 261 final ApplicationContext parent = new ClassPathXmlApplicationContext(parentContextPath); 262 parent.getAutowireCapableBeanFactory().autowireBeanProperties(this, 263 AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); 264 parent.getAutowireCapableBeanFactory().initializeBean(this, getClass().getSimpleName()); 265 this.parentContext = parent; 266 } 267 268 /** 269 * If embedded in a JVM, call this method to terminate the main method. 270 */ 271 public static void stop() { 272 synchronized (JobRegistryBackgroundJobRunner.class) { 273 JobRegistryBackgroundJobRunner.class.notify(); 274 } 275 } 276 277}