001/* 002 * Copyright 2012-2016 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 * http://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 */ 016 017package org.springframework.boot.cli.command.run; 018 019import java.io.File; 020import java.io.IOException; 021import java.lang.reflect.Method; 022import java.net.MalformedURLException; 023import java.net.URL; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.List; 027import java.util.concurrent.TimeUnit; 028import java.util.logging.Level; 029 030import org.springframework.boot.cli.app.SpringApplicationLauncher; 031import org.springframework.boot.cli.compiler.GroovyCompiler; 032import org.springframework.boot.cli.util.ResourceUtils; 033 034/** 035 * Compiles Groovy code running the resulting classes using a {@code SpringApplication}. 036 * Takes care of threading and class-loading issues and can optionally monitor sources for 037 * changes. 038 * 039 * @author Phillip Webb 040 * @author Dave Syer 041 */ 042public class SpringApplicationRunner { 043 044 private static int watcherCounter = 0; 045 046 private static int runnerCounter = 0; 047 048 private final Object monitor = new Object(); 049 050 private final SpringApplicationRunnerConfiguration configuration; 051 052 private final String[] sources; 053 054 private final String[] args; 055 056 private final GroovyCompiler compiler; 057 058 private RunThread runThread; 059 060 private FileWatchThread fileWatchThread; 061 062 /** 063 * Create a new {@link SpringApplicationRunner} instance. 064 * @param configuration the configuration 065 * @param sources the files to compile/watch 066 * @param args input arguments 067 */ 068 SpringApplicationRunner(final SpringApplicationRunnerConfiguration configuration, 069 String[] sources, String... args) { 070 this.configuration = configuration; 071 this.sources = sources.clone(); 072 this.args = args.clone(); 073 this.compiler = new GroovyCompiler(configuration); 074 int level = configuration.getLogLevel().intValue(); 075 if (level <= Level.FINER.intValue()) { 076 System.setProperty( 077 "org.springframework.boot.cli.compiler.grape.ProgressReporter", 078 "detail"); 079 System.setProperty("trace", "true"); 080 } 081 else if (level <= Level.FINE.intValue()) { 082 System.setProperty("debug", "true"); 083 } 084 else if (level == Level.OFF.intValue()) { 085 System.setProperty("spring.main.banner-mode", "OFF"); 086 System.setProperty("logging.level.ROOT", "OFF"); 087 System.setProperty( 088 "org.springframework.boot.cli.compiler.grape.ProgressReporter", 089 "none"); 090 } 091 } 092 093 /** 094 * Compile and run the application. 095 * @throws Exception on error 096 */ 097 public void compileAndRun() throws Exception { 098 synchronized (this.monitor) { 099 try { 100 stop(); 101 Object[] compiledSources = compile(); 102 monitorForChanges(); 103 // Run in new thread to ensure that the context classloader is setup 104 this.runThread = new RunThread(compiledSources); 105 this.runThread.start(); 106 this.runThread.join(); 107 } 108 catch (Exception ex) { 109 if (this.fileWatchThread == null) { 110 throw ex; 111 } 112 else { 113 ex.printStackTrace(); 114 } 115 } 116 } 117 } 118 119 public void stop() { 120 synchronized (this.monitor) { 121 if (this.runThread != null) { 122 this.runThread.shutdown(); 123 this.runThread = null; 124 } 125 } 126 } 127 128 private Object[] compile() throws IOException { 129 Object[] compiledSources = this.compiler.compile(this.sources); 130 if (compiledSources.length == 0) { 131 throw new RuntimeException( 132 "No classes found in '" + Arrays.toString(this.sources) + "'"); 133 } 134 return compiledSources; 135 } 136 137 private void monitorForChanges() { 138 if (this.fileWatchThread == null && this.configuration.isWatchForFileChanges()) { 139 this.fileWatchThread = new FileWatchThread(); 140 this.fileWatchThread.start(); 141 } 142 } 143 144 /** 145 * Thread used to launch the Spring Application with the correct context classloader. 146 */ 147 private class RunThread extends Thread { 148 149 private final Object monitor = new Object(); 150 151 private final Object[] compiledSources; 152 153 private Object applicationContext; 154 155 /** 156 * Create a new {@link RunThread} instance. 157 * @param compiledSources the sources to launch 158 */ 159 RunThread(Object... compiledSources) { 160 super("runner-" + (runnerCounter++)); 161 this.compiledSources = compiledSources; 162 if (compiledSources.length != 0 && compiledSources[0] instanceof Class) { 163 setContextClassLoader(((Class<?>) compiledSources[0]).getClassLoader()); 164 } 165 setDaemon(true); 166 } 167 168 @Override 169 public void run() { 170 synchronized (this.monitor) { 171 try { 172 this.applicationContext = new SpringApplicationLauncher( 173 getContextClassLoader()).launch(this.compiledSources, 174 SpringApplicationRunner.this.args); 175 } 176 catch (Exception ex) { 177 ex.printStackTrace(); 178 } 179 } 180 } 181 182 /** 183 * Shutdown the thread, closing any previously opened application context. 184 */ 185 public void shutdown() { 186 synchronized (this.monitor) { 187 if (this.applicationContext != null) { 188 try { 189 Method method = this.applicationContext.getClass() 190 .getMethod("close"); 191 method.invoke(this.applicationContext); 192 } 193 catch (NoSuchMethodException ex) { 194 // Not an application context that we can close 195 } 196 catch (Exception ex) { 197 ex.printStackTrace(); 198 } 199 finally { 200 this.applicationContext = null; 201 } 202 } 203 } 204 } 205 206 } 207 208 /** 209 * Thread to watch for file changes and trigger recompile/reload. 210 */ 211 private class FileWatchThread extends Thread { 212 213 private long previous; 214 215 private List<File> sources; 216 217 FileWatchThread() { 218 super("filewatcher-" + (watcherCounter++)); 219 this.previous = 0; 220 this.sources = getSourceFiles(); 221 for (File file : this.sources) { 222 if (file.exists()) { 223 long current = file.lastModified(); 224 if (current > this.previous) { 225 this.previous = current; 226 } 227 } 228 } 229 setDaemon(false); 230 } 231 232 private List<File> getSourceFiles() { 233 List<File> sources = new ArrayList<File>(); 234 for (String source : SpringApplicationRunner.this.sources) { 235 List<String> paths = ResourceUtils.getUrls(source, 236 SpringApplicationRunner.this.compiler.getLoader()); 237 for (String path : paths) { 238 try { 239 URL url = new URL(path); 240 if ("file".equals(url.getProtocol())) { 241 sources.add(new File(url.getFile())); 242 } 243 } 244 catch (MalformedURLException ex) { 245 // Ignore 246 } 247 } 248 } 249 return sources; 250 } 251 252 @Override 253 public void run() { 254 while (true) { 255 try { 256 Thread.sleep(TimeUnit.SECONDS.toMillis(1)); 257 for (File file : this.sources) { 258 if (file.exists()) { 259 long current = file.lastModified(); 260 if (this.previous < current) { 261 this.previous = current; 262 compileAndRun(); 263 } 264 } 265 } 266 } 267 catch (InterruptedException ex) { 268 Thread.currentThread().interrupt(); 269 } 270 catch (Exception ex) { 271 // Swallow, will be reported by compileAndRun 272 } 273 } 274 } 275 276 } 277 278}