View Javadoc

1   /*
2    * Copyright 2006-2013 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.springframework.batch.core.launch.support;
17  
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collections;
22  import java.util.List;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.springframework.batch.core.Job;
27  import org.springframework.batch.core.configuration.DuplicateJobException;
28  import org.springframework.batch.core.configuration.JobFactory;
29  import org.springframework.batch.core.configuration.JobRegistry;
30  import org.springframework.batch.core.configuration.support.DefaultJobLoader;
31  import org.springframework.batch.core.configuration.support.GenericApplicationContextFactory;
32  import org.springframework.batch.core.configuration.support.JobLoader;
33  import org.springframework.batch.core.launch.JobLauncher;
34  import org.springframework.beans.factory.BeanFactory;
35  import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
36  import org.springframework.context.ApplicationContext;
37  import org.springframework.context.support.ClassPathXmlApplicationContext;
38  import org.springframework.core.io.Resource;
39  import org.springframework.util.Assert;
40  
41  /**
42   * <p>
43   * Command line launcher for registering jobs with a {@link JobRegistry}.
44   * Normally this will be used in conjunction with an external trigger for the
45   * jobs registered, e.g. a JMX MBean wrapper for a {@link JobLauncher}, or a
46   * Quartz trigger.
47   * </p>
48   *
49   * <p>
50   * With any launch of a batch job within Spring Batch, a Spring context
51   * containing the {@link Job} has to be created. Using this launcher, the jobs
52   * are all registered with a {@link JobRegistry} defined in a parent application
53   * context. The jobs are then set up in child contexts. All dependencies of the
54   * runner will then be satisfied by autowiring by type from the parent
55   * application context. Default values are provided for all fields except the
56   * {@link JobRegistry}. Therefore, if autowiring fails to set it then an
57   * exception will be thrown.
58   * </p>
59   *
60   * @author Dave Syer
61   *
62   */
63  public class JobRegistryBackgroundJobRunner {
64  
65  	/**
66  	 * System property key that switches the runner to "embedded" mode
67  	 * (returning immediately from the main method). Useful for testing
68  	 * purposes.
69  	 */
70  	public static final String EMBEDDED = JobRegistryBackgroundJobRunner.class.getSimpleName() + ".EMBEDDED";
71  
72  	private static Log logger = LogFactory.getLog(JobRegistryBackgroundJobRunner.class);
73  
74  	private JobLoader jobLoader;
75  
76  	private ApplicationContext parentContext = null;
77  
78  	public static boolean testing = false;
79  
80  	final private String parentContextPath;
81  
82  	private JobRegistry jobRegistry;
83  
84  	private static List<Exception> errors = Collections.synchronizedList(new ArrayList<Exception>());
85  
86  	/**
87  	 * @param parentContextPath
88  	 */
89  	public JobRegistryBackgroundJobRunner(String parentContextPath) {
90  		super();
91  		this.parentContextPath = parentContextPath;
92  	}
93  
94  	/**
95  	 * A loader for the jobs that are going to be registered.
96  	 *
97  	 * @param jobLoader the {@link JobLoader} to set
98  	 */
99  	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 	 * De-register all the {@link Job} instances that were regsistered 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 }