/
GroovyScriptFactory.java
375 lines (325 loc) · 12.6 KB
/
GroovyScriptFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scripting.groovy;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import groovy.lang.Script;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.lang.Nullable;
import org.springframework.scripting.ScriptCompilationException;
import org.springframework.scripting.ScriptFactory;
import org.springframework.scripting.ScriptSource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@link org.springframework.scripting.ScriptFactory} implementation
* for a Groovy script.
*
* <p>Typically used in combination with a
* {@link org.springframework.scripting.support.ScriptFactoryPostProcessor};
* see the latter's javadoc for a configuration example.
*
* <p>Note: Spring 4.0 supports Groovy 1.8 and higher.
*
* @author Juergen Hoeller
* @author Rob Harrop
* @author Rod Johnson
* @since 2.0
* @see groovy.lang.GroovyClassLoader
* @see org.springframework.scripting.support.ScriptFactoryPostProcessor
*/
public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, BeanClassLoaderAware {
private final String scriptSourceLocator;
@Nullable
private GroovyObjectCustomizer groovyObjectCustomizer;
@Nullable
private CompilerConfiguration compilerConfiguration;
@Nullable
private GroovyClassLoader groovyClassLoader;
@Nullable
private Class<?> scriptClass;
@Nullable
private Class<?> scriptResultClass;
@Nullable
private CachedResultHolder cachedResult;
private final Object scriptClassMonitor = new Object();
private boolean wasModifiedForTypeCheck = false;
/**
* Create a new GroovyScriptFactory for the given script source.
* <p>We don't need to specify script interfaces here, since
* a Groovy script defines its Java interfaces itself.
* @param scriptSourceLocator a locator that points to the source of the script.
* Interpreted by the post-processor that actually creates the script.
*/
public GroovyScriptFactory(String scriptSourceLocator) {
Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
this.scriptSourceLocator = scriptSourceLocator;
}
/**
* Create a new GroovyScriptFactory for the given script source,
* specifying a strategy interface that can create a custom MetaClass
* to supply missing methods and otherwise change the behavior of the object.
* @param scriptSourceLocator a locator that points to the source of the script.
* Interpreted by the post-processor that actually creates the script.
* @param groovyObjectCustomizer a customizer that can set a custom metaclass
* or make other changes to the GroovyObject created by this factory
* (may be {@code null})
* @see GroovyObjectCustomizer#customize
*/
public GroovyScriptFactory(String scriptSourceLocator, @Nullable GroovyObjectCustomizer groovyObjectCustomizer) {
this(scriptSourceLocator);
this.groovyObjectCustomizer = groovyObjectCustomizer;
}
/**
* Create a new GroovyScriptFactory for the given script source,
* specifying a strategy interface that can create a custom MetaClass
* to supply missing methods and otherwise change the behavior of the object.
* @param scriptSourceLocator a locator that points to the source of the script.
* Interpreted by the post-processor that actually creates the script.
* @param compilerConfiguration a custom compiler configuration to be applied
* to the GroovyClassLoader (may be {@code null})
* @since 4.3.3
* @see GroovyClassLoader#GroovyClassLoader(ClassLoader, CompilerConfiguration)
*/
public GroovyScriptFactory(String scriptSourceLocator, @Nullable CompilerConfiguration compilerConfiguration) {
this(scriptSourceLocator);
this.compilerConfiguration = compilerConfiguration;
}
/**
* Create a new GroovyScriptFactory for the given script source,
* specifying a strategy interface that can customize Groovy's compilation
* process within the underlying GroovyClassLoader.
* @param scriptSourceLocator a locator that points to the source of the script.
* Interpreted by the post-processor that actually creates the script.
* @param compilationCustomizers one or more customizers to be applied to the
* GroovyClassLoader compiler configuration
* @since 4.3.3
* @see CompilerConfiguration#addCompilationCustomizers
* @see org.codehaus.groovy.control.customizers.ImportCustomizer
*/
public GroovyScriptFactory(String scriptSourceLocator, CompilationCustomizer... compilationCustomizers) {
this(scriptSourceLocator);
if (!ObjectUtils.isEmpty(compilationCustomizers)) {
this.compilerConfiguration = new CompilerConfiguration();
this.compilerConfiguration.addCompilationCustomizers(compilationCustomizers);
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
((ConfigurableListableBeanFactory) beanFactory).ignoreDependencyType(MetaClass.class);
}
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
if (classLoader instanceof GroovyClassLoader &&
(this.compilerConfiguration == null ||
((GroovyClassLoader) classLoader).hasCompatibleConfiguration(this.compilerConfiguration))) {
this.groovyClassLoader = (GroovyClassLoader) classLoader;
}
else {
this.groovyClassLoader = buildGroovyClassLoader(classLoader);
}
}
/**
* Return the GroovyClassLoader used by this script factory.
*/
public GroovyClassLoader getGroovyClassLoader() {
synchronized (this.scriptClassMonitor) {
if (this.groovyClassLoader == null) {
this.groovyClassLoader = buildGroovyClassLoader(ClassUtils.getDefaultClassLoader());
}
return this.groovyClassLoader;
}
}
/**
* Build a {@link GroovyClassLoader} for the given {@code ClassLoader}.
* @param classLoader the ClassLoader to build a GroovyClassLoader for
* @since 4.3.3
*/
protected GroovyClassLoader buildGroovyClassLoader(@Nullable ClassLoader classLoader) {
return (this.compilerConfiguration != null ?
new GroovyClassLoader(classLoader, this.compilerConfiguration) : new GroovyClassLoader(classLoader));
}
@Override
public String getScriptSourceLocator() {
return this.scriptSourceLocator;
}
/**
* Groovy scripts determine their interfaces themselves,
* hence we don't need to explicitly expose interfaces here.
* @return {@code null} always
*/
@Override
@Nullable
public Class<?>[] getScriptInterfaces() {
return null;
}
/**
* Groovy scripts do not need a config interface,
* since they expose their setters as public methods.
*/
@Override
public boolean requiresConfigInterface() {
return false;
}
/**
* Loads and parses the Groovy script via the GroovyClassLoader.
* @see groovy.lang.GroovyClassLoader
*/
@Override
@Nullable
public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class<?>... actualInterfaces)
throws IOException, ScriptCompilationException {
synchronized (this.scriptClassMonitor) {
try {
Class<?> scriptClassToExecute;
this.wasModifiedForTypeCheck = false;
if (this.cachedResult != null) {
Object result = this.cachedResult.object;
this.cachedResult = null;
return result;
}
if (this.scriptClass == null || scriptSource.isModified()) {
// New script content...
this.scriptClass = getGroovyClassLoader().parseClass(
scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
if (Script.class.isAssignableFrom(this.scriptClass)) {
// A Groovy script, probably creating an instance: let's execute it.
Object result = executeScript(scriptSource, this.scriptClass);
this.scriptResultClass = (result != null ? result.getClass() : null);
return result;
}
else {
this.scriptResultClass = this.scriptClass;
}
}
scriptClassToExecute = this.scriptClass;
// Process re-execution outside of the synchronized block.
return executeScript(scriptSource, scriptClassToExecute);
}
catch (CompilationFailedException ex) {
this.scriptClass = null;
this.scriptResultClass = null;
throw new ScriptCompilationException(scriptSource, ex);
}
}
}
@Override
@Nullable
public Class<?> getScriptedObjectType(ScriptSource scriptSource)
throws IOException, ScriptCompilationException {
synchronized (this.scriptClassMonitor) {
try {
if (this.scriptClass == null || scriptSource.isModified()) {
// New script content...
this.wasModifiedForTypeCheck = true;
this.scriptClass = getGroovyClassLoader().parseClass(
scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
if (Script.class.isAssignableFrom(this.scriptClass)) {
// A Groovy script, probably creating an instance: let's execute it.
Object result = executeScript(scriptSource, this.scriptClass);
this.scriptResultClass = (result != null ? result.getClass() : null);
this.cachedResult = new CachedResultHolder(result);
}
else {
this.scriptResultClass = this.scriptClass;
}
}
return this.scriptResultClass;
}
catch (CompilationFailedException ex) {
this.scriptClass = null;
this.scriptResultClass = null;
this.cachedResult = null;
throw new ScriptCompilationException(scriptSource, ex);
}
}
}
@Override
public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
synchronized (this.scriptClassMonitor) {
return (scriptSource.isModified() || this.wasModifiedForTypeCheck);
}
}
/**
* Instantiate the given Groovy script class and run it if necessary.
* @param scriptSource the source for the underlying script
* @param scriptClass the Groovy script class
* @return the result object (either an instance of the script class
* or the result of running the script instance)
* @throws ScriptCompilationException in case of instantiation failure
*/
@Nullable
protected Object executeScript(ScriptSource scriptSource, Class<?> scriptClass) throws ScriptCompilationException {
try {
GroovyObject goo = (GroovyObject) ReflectionUtils.accessibleConstructor(scriptClass).newInstance();
if (this.groovyObjectCustomizer != null) {
// Allow metaclass and other customization.
this.groovyObjectCustomizer.customize(goo);
}
if (goo instanceof Script) {
// A Groovy script, probably creating an instance: let's execute it.
return ((Script) goo).run();
}
else {
// An instance of the scripted class: let's return it as-is.
return goo;
}
}
catch (NoSuchMethodException ex) {
throw new ScriptCompilationException(
"No default constructor on Groovy script class: " + scriptClass.getName(), ex);
}
catch (InstantiationException ex) {
throw new ScriptCompilationException(
scriptSource, "Unable to instantiate Groovy script class: " + scriptClass.getName(), ex);
}
catch (IllegalAccessException ex) {
throw new ScriptCompilationException(
scriptSource, "Could not access Groovy script constructor: " + scriptClass.getName(), ex);
}
catch (InvocationTargetException ex) {
throw new ScriptCompilationException(
"Failed to invoke Groovy script constructor: " + scriptClass.getName(), ex.getTargetException());
}
}
@Override
public String toString() {
return "GroovyScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
}
/**
* Wrapper that holds a temporarily cached result object.
*/
private static class CachedResultHolder {
@Nullable
public final Object object;
public CachedResultHolder(@Nullable Object object) {
this.object = object;
}
}
}