001/* 002 * Copyright 2002-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 * 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 */ 016 017package org.springframework.scripting.groovy; 018 019import java.io.IOException; 020import java.lang.reflect.InvocationTargetException; 021 022import groovy.lang.GroovyClassLoader; 023import groovy.lang.GroovyObject; 024import groovy.lang.MetaClass; 025import groovy.lang.Script; 026import org.codehaus.groovy.control.CompilationFailedException; 027import org.codehaus.groovy.control.CompilerConfiguration; 028import org.codehaus.groovy.control.customizers.CompilationCustomizer; 029 030import org.springframework.beans.factory.BeanClassLoaderAware; 031import org.springframework.beans.factory.BeanFactory; 032import org.springframework.beans.factory.BeanFactoryAware; 033import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 034import org.springframework.scripting.ScriptCompilationException; 035import org.springframework.scripting.ScriptFactory; 036import org.springframework.scripting.ScriptSource; 037import org.springframework.util.Assert; 038import org.springframework.util.ClassUtils; 039import org.springframework.util.ObjectUtils; 040import org.springframework.util.ReflectionUtils; 041 042/** 043 * {@link org.springframework.scripting.ScriptFactory} implementation 044 * for a Groovy script. 045 * 046 * <p>Typically used in combination with a 047 * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; 048 * see the latter's javadoc for a configuration example. 049 * 050 * <p>Note: Spring 4.0 supports Groovy 1.8 and higher. 051 * 052 * @author Juergen Hoeller 053 * @author Rob Harrop 054 * @author Rod Johnson 055 * @since 2.0 056 * @see groovy.lang.GroovyClassLoader 057 * @see org.springframework.scripting.support.ScriptFactoryPostProcessor 058 */ 059public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, BeanClassLoaderAware { 060 061 private final String scriptSourceLocator; 062 063 private GroovyObjectCustomizer groovyObjectCustomizer; 064 065 private CompilerConfiguration compilerConfiguration; 066 067 private GroovyClassLoader groovyClassLoader; 068 069 private Class<?> scriptClass; 070 071 private Class<?> scriptResultClass; 072 073 private CachedResultHolder cachedResult; 074 075 private final Object scriptClassMonitor = new Object(); 076 077 private boolean wasModifiedForTypeCheck = false; 078 079 080 /** 081 * Create a new GroovyScriptFactory for the given script source. 082 * <p>We don't need to specify script interfaces here, since 083 * a Groovy script defines its Java interfaces itself. 084 * @param scriptSourceLocator a locator that points to the source of the script. 085 * Interpreted by the post-processor that actually creates the script. 086 */ 087 public GroovyScriptFactory(String scriptSourceLocator) { 088 Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); 089 this.scriptSourceLocator = scriptSourceLocator; 090 } 091 092 /** 093 * Create a new GroovyScriptFactory for the given script source, 094 * specifying a strategy interface that can create a custom MetaClass 095 * to supply missing methods and otherwise change the behavior of the object. 096 * @param scriptSourceLocator a locator that points to the source of the script. 097 * Interpreted by the post-processor that actually creates the script. 098 * @param groovyObjectCustomizer a customizer that can set a custom metaclass 099 * or make other changes to the GroovyObject created by this factory 100 * (may be {@code null}) 101 * @see GroovyObjectCustomizer#customize 102 */ 103 public GroovyScriptFactory(String scriptSourceLocator, GroovyObjectCustomizer groovyObjectCustomizer) { 104 this(scriptSourceLocator); 105 this.groovyObjectCustomizer = groovyObjectCustomizer; 106 } 107 108 /** 109 * Create a new GroovyScriptFactory for the given script source, 110 * specifying a strategy interface that can create a custom MetaClass 111 * to supply missing methods and otherwise change the behavior of the object. 112 * @param scriptSourceLocator a locator that points to the source of the script. 113 * Interpreted by the post-processor that actually creates the script. 114 * @param compilerConfiguration a custom compiler configuration to be applied 115 * to the GroovyClassLoader (may be {@code null}) 116 * @since 4.3.3 117 * @see GroovyClassLoader#GroovyClassLoader(ClassLoader, CompilerConfiguration) 118 */ 119 public GroovyScriptFactory(String scriptSourceLocator, CompilerConfiguration compilerConfiguration) { 120 this(scriptSourceLocator); 121 this.compilerConfiguration = compilerConfiguration; 122 } 123 124 /** 125 * Create a new GroovyScriptFactory for the given script source, 126 * specifying a strategy interface that can customize Groovy's compilation 127 * process within the underlying GroovyClassLoader. 128 * @param scriptSourceLocator a locator that points to the source of the script. 129 * Interpreted by the post-processor that actually creates the script. 130 * @param compilationCustomizers one or more customizers to be applied to the 131 * GroovyClassLoader compiler configuration 132 * @since 4.3.3 133 * @see CompilerConfiguration#addCompilationCustomizers 134 * @see org.codehaus.groovy.control.customizers.ImportCustomizer 135 */ 136 public GroovyScriptFactory(String scriptSourceLocator, CompilationCustomizer... compilationCustomizers) { 137 this(scriptSourceLocator); 138 if (!ObjectUtils.isEmpty(compilationCustomizers)) { 139 this.compilerConfiguration = new CompilerConfiguration(); 140 this.compilerConfiguration.addCompilationCustomizers(compilationCustomizers); 141 } 142 } 143 144 145 @Override 146 public void setBeanFactory(BeanFactory beanFactory) { 147 if (beanFactory instanceof ConfigurableListableBeanFactory) { 148 ((ConfigurableListableBeanFactory) beanFactory).ignoreDependencyType(MetaClass.class); 149 } 150 } 151 152 @Override 153 public void setBeanClassLoader(ClassLoader classLoader) { 154 this.groovyClassLoader = buildGroovyClassLoader(classLoader); 155 } 156 157 /** 158 * Return the GroovyClassLoader used by this script factory. 159 */ 160 public GroovyClassLoader getGroovyClassLoader() { 161 synchronized (this.scriptClassMonitor) { 162 if (this.groovyClassLoader == null) { 163 this.groovyClassLoader = buildGroovyClassLoader(ClassUtils.getDefaultClassLoader()); 164 } 165 return this.groovyClassLoader; 166 } 167 } 168 169 /** 170 * Build a {@link GroovyClassLoader} for the given {@code ClassLoader}. 171 * @param classLoader the ClassLoader to build a GroovyClassLoader for 172 * @since 4.3.3 173 */ 174 protected GroovyClassLoader buildGroovyClassLoader(ClassLoader classLoader) { 175 return (this.compilerConfiguration != null ? 176 new GroovyClassLoader(classLoader, this.compilerConfiguration) : new GroovyClassLoader(classLoader)); 177 } 178 179 180 @Override 181 public String getScriptSourceLocator() { 182 return this.scriptSourceLocator; 183 } 184 185 /** 186 * Groovy scripts determine their interfaces themselves, 187 * hence we don't need to explicitly expose interfaces here. 188 * @return {@code null} always 189 */ 190 @Override 191 public Class<?>[] getScriptInterfaces() { 192 return null; 193 } 194 195 /** 196 * Groovy scripts do not need a config interface, 197 * since they expose their setters as public methods. 198 */ 199 @Override 200 public boolean requiresConfigInterface() { 201 return false; 202 } 203 204 205 /** 206 * Loads and parses the Groovy script via the GroovyClassLoader. 207 * @see groovy.lang.GroovyClassLoader 208 */ 209 @Override 210 public Object getScriptedObject(ScriptSource scriptSource, Class<?>... actualInterfaces) 211 throws IOException, ScriptCompilationException { 212 213 synchronized (this.scriptClassMonitor) { 214 try { 215 Class<?> scriptClassToExecute; 216 this.wasModifiedForTypeCheck = false; 217 218 if (this.cachedResult != null) { 219 Object result = this.cachedResult.object; 220 this.cachedResult = null; 221 return result; 222 } 223 224 if (this.scriptClass == null || scriptSource.isModified()) { 225 // New script content... 226 this.scriptClass = getGroovyClassLoader().parseClass( 227 scriptSource.getScriptAsString(), scriptSource.suggestedClassName()); 228 229 if (Script.class.isAssignableFrom(this.scriptClass)) { 230 // A Groovy script, probably creating an instance: let's execute it. 231 Object result = executeScript(scriptSource, this.scriptClass); 232 this.scriptResultClass = (result != null ? result.getClass() : null); 233 return result; 234 } 235 else { 236 this.scriptResultClass = this.scriptClass; 237 } 238 } 239 scriptClassToExecute = this.scriptClass; 240 241 // Process re-execution outside of the synchronized block. 242 return executeScript(scriptSource, scriptClassToExecute); 243 } 244 catch (CompilationFailedException ex) { 245 this.scriptClass = null; 246 this.scriptResultClass = null; 247 throw new ScriptCompilationException(scriptSource, ex); 248 } 249 } 250 } 251 252 @Override 253 public Class<?> getScriptedObjectType(ScriptSource scriptSource) 254 throws IOException, ScriptCompilationException { 255 256 synchronized (this.scriptClassMonitor) { 257 try { 258 if (this.scriptClass == null || scriptSource.isModified()) { 259 // New script content... 260 this.wasModifiedForTypeCheck = true; 261 this.scriptClass = getGroovyClassLoader().parseClass( 262 scriptSource.getScriptAsString(), scriptSource.suggestedClassName()); 263 264 if (Script.class.isAssignableFrom(this.scriptClass)) { 265 // A Groovy script, probably creating an instance: let's execute it. 266 Object result = executeScript(scriptSource, this.scriptClass); 267 this.scriptResultClass = (result != null ? result.getClass() : null); 268 this.cachedResult = new CachedResultHolder(result); 269 } 270 else { 271 this.scriptResultClass = this.scriptClass; 272 } 273 } 274 return this.scriptResultClass; 275 } 276 catch (CompilationFailedException ex) { 277 this.scriptClass = null; 278 this.scriptResultClass = null; 279 this.cachedResult = null; 280 throw new ScriptCompilationException(scriptSource, ex); 281 } 282 } 283 } 284 285 @Override 286 public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { 287 synchronized (this.scriptClassMonitor) { 288 return (scriptSource.isModified() || this.wasModifiedForTypeCheck); 289 } 290 } 291 292 293 /** 294 * Instantiate the given Groovy script class and run it if necessary. 295 * @param scriptSource the source for the underlying script 296 * @param scriptClass the Groovy script class 297 * @return the result object (either an instance of the script class 298 * or the result of running the script instance) 299 * @throws ScriptCompilationException in case of instantiation failure 300 */ 301 protected Object executeScript(ScriptSource scriptSource, Class<?> scriptClass) throws ScriptCompilationException { 302 try { 303 304 305 if (this.groovyObjectCustomizer != null) { 306 // Allow metaclass and other customization. 307 this.groovyObjectCustomizer.customize(goo); 308 } 309 310 if (goo instanceof Script) { 311 // A Groovy script, probably creating an instance: let's execute it. 312 return ((Script) goo).run(); 313 } 314 else { 315 // An instance of the scripted class: let's return it as-is. 316 return goo; 317 } 318 } 319 catch (InstantiationException ex) { 320 throw new ScriptCompilationException( 321 scriptSource, "Unable to instantiate Groovy script class: " + scriptClass.getName(), ex); 322 } 323 catch (IllegalAccessException ex) { 324 throw new ScriptCompilationException( 325 scriptSource, "Could not access Groovy script constructor: " + scriptClass.getName(), ex); 326 } 327 } 328 329 330 @Override 331 public String toString() { 332 return "GroovyScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; 333 } 334 335 336 /** 337 * Wrapper that holds a temporarily cached result object. 338 */ 339 private static class CachedResultHolder { 340 341 public final Object object; 342 343 public CachedResultHolder(Object object) { 344 this.object = object; 345 } 346 } 347 348}