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