• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

49.62
today-context/src/main/java/infra/scripting/groovy/GroovyScriptFactory.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.scripting.groovy;
19

20
import org.codehaus.groovy.control.CompilationFailedException;
21
import org.codehaus.groovy.control.CompilerConfiguration;
22
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
23
import org.jspecify.annotations.Nullable;
24

25
import java.io.IOException;
26
import java.lang.reflect.InaccessibleObjectException;
27
import java.lang.reflect.InvocationTargetException;
28

29
import groovy.lang.GroovyClassLoader;
30
import groovy.lang.GroovyObject;
31
import groovy.lang.MetaClass;
32
import groovy.lang.Script;
33
import infra.beans.factory.BeanClassLoaderAware;
34
import infra.beans.factory.BeanFactory;
35
import infra.beans.factory.BeanFactoryAware;
36
import infra.beans.factory.config.ConfigurableBeanFactory;
37
import infra.core.ConstructorNotFoundException;
38
import infra.lang.Assert;
39
import infra.scripting.ScriptCompilationException;
40
import infra.scripting.ScriptFactory;
41
import infra.scripting.ScriptSource;
42
import infra.scripting.support.ScriptFactoryPostProcessor;
43
import infra.util.ClassUtils;
44
import infra.util.ObjectUtils;
45
import infra.util.ReflectionUtils;
46

47
/**
48
 * {@link ScriptFactory} implementation
49
 * for a Groovy script.
50
 *
51
 * <p>Typically used in combination with a
52
 * {@link ScriptFactoryPostProcessor};
53
 * see the latter's javadoc for a configuration example.
54
 *
55
 * <p>supports Groovy 1.8 and higher.
56
 *
57
 * @author Juergen Hoeller
58
 * @author Rob Harrop
59
 * @author Rod Johnson
60
 * @author <a href="https://github.com/TAKETODAY">海子 Yang</a>
61
 * @see groovy.lang.GroovyClassLoader
62
 * @see ScriptFactoryPostProcessor
63
 * @since 4.0
64
 */
65
public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, BeanClassLoaderAware {
66

67
  private final String scriptSourceLocator;
68

69
  @Nullable
70
  private GroovyObjectCustomizer groovyObjectCustomizer;
71

72
  @Nullable
73
  private CompilerConfiguration compilerConfiguration;
74

75
  @Nullable
76
  private GroovyClassLoader groovyClassLoader;
77

78
  @Nullable
79
  private Class<?> scriptClass;
80

81
  @Nullable
82
  private Class<?> scriptResultClass;
83

84
  @Nullable
85
  private CachedResultHolder cachedResult;
86

87
  private final Object scriptClassMonitor = new Object();
5✔
88

89
  private boolean wasModifiedForTypeCheck = false;
3✔
90

91
  /**
92
   * Create a new GroovyScriptFactory for the given script source.
93
   * <p>We don't need to specify script interfaces here, since
94
   * a Groovy script defines its Java interfaces itself.
95
   *
96
   * @param scriptSourceLocator a locator that points to the source of the script.
97
   * Interpreted by the post-processor that actually creates the script.
98
   */
99
  public GroovyScriptFactory(String scriptSourceLocator) {
2✔
100
    Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
3✔
101
    this.scriptSourceLocator = scriptSourceLocator;
3✔
102
  }
1✔
103

104
  /**
105
   * Create a new GroovyScriptFactory for the given script source,
106
   * specifying a strategy interface that can create a custom MetaClass
107
   * to supply missing methods and otherwise change the behavior of the object.
108
   *
109
   * @param scriptSourceLocator a locator that points to the source of the script.
110
   * Interpreted by the post-processor that actually creates the script.
111
   * @param groovyObjectCustomizer a customizer that can set a custom metaclass
112
   * or make other changes to the GroovyObject created by this factory
113
   * (may be {@code null})
114
   * @see GroovyObjectCustomizer#customize
115
   */
116
  public GroovyScriptFactory(String scriptSourceLocator, @Nullable GroovyObjectCustomizer groovyObjectCustomizer) {
117
    this(scriptSourceLocator);
×
118
    this.groovyObjectCustomizer = groovyObjectCustomizer;
×
119
  }
×
120

121
  /**
122
   * Create a new GroovyScriptFactory for the given script source,
123
   * specifying a strategy interface that can create a custom MetaClass
124
   * to supply missing methods and otherwise change the behavior of the object.
125
   *
126
   * @param scriptSourceLocator a locator that points to the source of the script.
127
   * Interpreted by the post-processor that actually creates the script.
128
   * @param compilerConfiguration a custom compiler configuration to be applied
129
   * to the GroovyClassLoader (may be {@code null})
130
   * @see GroovyClassLoader#GroovyClassLoader(ClassLoader, CompilerConfiguration)
131
   */
132
  public GroovyScriptFactory(String scriptSourceLocator, @Nullable CompilerConfiguration compilerConfiguration) {
133
    this(scriptSourceLocator);
×
134
    this.compilerConfiguration = compilerConfiguration;
×
135
  }
×
136

137
  /**
138
   * Create a new GroovyScriptFactory for the given script source,
139
   * specifying a strategy interface that can customize Groovy's compilation
140
   * process within the underlying GroovyClassLoader.
141
   *
142
   * @param scriptSourceLocator a locator that points to the source of the script.
143
   * Interpreted by the post-processor that actually creates the script.
144
   * @param compilationCustomizers one or more customizers to be applied to the
145
   * GroovyClassLoader compiler configuration
146
   * @see CompilerConfiguration#addCompilationCustomizers
147
   * @see org.codehaus.groovy.control.customizers.ImportCustomizer
148
   */
149
  public GroovyScriptFactory(String scriptSourceLocator, CompilationCustomizer... compilationCustomizers) {
150
    this(scriptSourceLocator);
×
151
    if (ObjectUtils.isNotEmpty(compilationCustomizers)) {
×
152
      this.compilerConfiguration = new CompilerConfiguration();
×
153
      this.compilerConfiguration.addCompilationCustomizers(compilationCustomizers);
×
154
    }
155
  }
×
156

157
  @Override
158
  public void setBeanFactory(BeanFactory beanFactory) {
159
    if (beanFactory instanceof ConfigurableBeanFactory) {
3!
160
      ((ConfigurableBeanFactory) beanFactory).ignoreDependencyType(MetaClass.class);
4✔
161
    }
162
  }
1✔
163

164
  @Override
165
  public void setBeanClassLoader(ClassLoader classLoader) {
166
    if (classLoader instanceof GroovyClassLoader &&
3!
167
            (this.compilerConfiguration == null ||
168
                    ((GroovyClassLoader) classLoader).hasCompatibleConfiguration(this.compilerConfiguration))) {
×
169
      this.groovyClassLoader = (GroovyClassLoader) classLoader;
×
170
    }
171
    else {
172
      this.groovyClassLoader = buildGroovyClassLoader(classLoader);
5✔
173
    }
174
  }
1✔
175

176
  /**
177
   * Return the GroovyClassLoader used by this script factory.
178
   */
179
  public GroovyClassLoader getGroovyClassLoader() {
180
    synchronized(this.scriptClassMonitor) {
5✔
181
      if (this.groovyClassLoader == null) {
3!
182
        this.groovyClassLoader = buildGroovyClassLoader(ClassUtils.getDefaultClassLoader());
×
183
      }
184
      return this.groovyClassLoader;
5✔
185
    }
186
  }
187

188
  /**
189
   * Build a {@link GroovyClassLoader} for the given {@code ClassLoader}.
190
   *
191
   * @param classLoader the ClassLoader to build a GroovyClassLoader for
192
   */
193
  protected GroovyClassLoader buildGroovyClassLoader(@Nullable ClassLoader classLoader) {
194
    return compilerConfiguration != null ?
4!
195
            new GroovyClassLoader(classLoader, this.compilerConfiguration) : new GroovyClassLoader(classLoader);
4✔
196
  }
197

198
  @Override
199
  public String getScriptSourceLocator() {
200
    return this.scriptSourceLocator;
3✔
201
  }
202

203
  /**
204
   * Groovy scripts determine their interfaces themselves,
205
   * hence we don't need to explicitly expose interfaces here.
206
   *
207
   * @return {@code null} always
208
   */
209
  @Override
210
  public Class<?> @Nullable [] getScriptInterfaces() {
211
    return null;
2✔
212
  }
213

214
  /**
215
   * Groovy scripts do not need a config interface,
216
   * since they expose their setters as public methods.
217
   */
218
  @Override
219
  public boolean requiresConfigInterface() {
220
    return false;
2✔
221
  }
222

223
  /**
224
   * Loads and parses the Groovy script via the GroovyClassLoader.
225
   *
226
   * @see groovy.lang.GroovyClassLoader
227
   */
228
  @Override
229
  @Nullable
230
  public Object getScriptedObject(ScriptSource scriptSource, Class<?> @Nullable ... actualInterfaces)
231
          throws IOException, ScriptCompilationException {
232

233
    synchronized(this.scriptClassMonitor) {
5✔
234
      try {
235
        Class<?> scriptClassToExecute;
236
        this.wasModifiedForTypeCheck = false;
3✔
237

238
        if (this.cachedResult != null) {
3!
239
          Object result = this.cachedResult.object;
×
240
          this.cachedResult = null;
×
241
          return result;
×
242
        }
243

244
        if (this.scriptClass == null || scriptSource.isModified()) {
6!
245
          // New script content...
246
          this.scriptClass = getGroovyClassLoader().parseClass(
6✔
247
                  scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
3✔
248

249
          if (Script.class.isAssignableFrom(this.scriptClass)) {
5!
250
            // A Groovy script, probably creating an instance: let's execute it.
251
            Object result = executeScript(scriptSource, this.scriptClass);
×
252
            this.scriptResultClass = (result != null ? result.getClass() : null);
×
253
            return result;
×
254
          }
255
          else {
256
            this.scriptResultClass = this.scriptClass;
4✔
257
          }
258
        }
259
        scriptClassToExecute = this.scriptClass;
3✔
260

261
        // Process re-execution outside of the synchronized block.
262
        return executeScript(scriptSource, scriptClassToExecute);
7✔
263
      }
264
      catch (CompilationFailedException ex) {
1✔
265
        this.scriptClass = null;
3✔
266
        this.scriptResultClass = null;
3✔
267
        throw new ScriptCompilationException(scriptSource, ex);
6✔
268
      }
269
    }
270
  }
271

272
  @Override
273
  @Nullable
274
  public Class<?> getScriptedObjectType(ScriptSource scriptSource)
275
          throws IOException, ScriptCompilationException {
276

277
    synchronized(this.scriptClassMonitor) {
5✔
278
      try {
279
        if (this.scriptClass == null || scriptSource.isModified()) {
6!
280
          // New script content...
281
          this.wasModifiedForTypeCheck = true;
3✔
282
          this.scriptClass = getGroovyClassLoader().parseClass(
6✔
283
                  scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
3✔
284

285
          if (Script.class.isAssignableFrom(this.scriptClass)) {
5!
286
            // A Groovy script, probably creating an instance: let's execute it.
287
            Object result = executeScript(scriptSource, this.scriptClass);
×
288
            this.scriptResultClass = (result != null ? result.getClass() : null);
×
289
            this.cachedResult = new CachedResultHolder(result);
×
290
          }
×
291
          else {
292
            this.scriptResultClass = this.scriptClass;
4✔
293
          }
294
        }
295
        return this.scriptResultClass;
5✔
296
      }
297
      catch (CompilationFailedException ex) {
×
298
        this.scriptClass = null;
×
299
        this.scriptResultClass = null;
×
300
        this.cachedResult = null;
×
301
        throw new ScriptCompilationException(scriptSource, ex);
×
302
      }
303
    }
304
  }
305

306
  @Override
307
  public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
308
    synchronized(this.scriptClassMonitor) {
5✔
309
      return scriptSource.isModified() || this.wasModifiedForTypeCheck;
12!
310
    }
311
  }
312

313
  /**
314
   * Instantiate the given Groovy script class and run it if necessary.
315
   *
316
   * @param scriptSource the source for the underlying script
317
   * @param scriptClass the Groovy script class
318
   * @return the result object (either an instance of the script class
319
   * or the result of running the script instance)
320
   * @throws ScriptCompilationException in case of instantiation failure
321
   */
322
  @Nullable
323
  protected Object executeScript(ScriptSource scriptSource, Class<?> scriptClass) throws ScriptCompilationException {
324
    try {
325
      GroovyObject goo = (GroovyObject) ReflectionUtils.accessibleConstructor(scriptClass).newInstance();
9✔
326

327
      if (groovyObjectCustomizer != null) {
3!
328
        // Allow metaclass and other customization.
329
        groovyObjectCustomizer.customize(goo);
×
330
      }
331

332
      if (goo instanceof Script) {
3!
333
        // A Groovy script, probably creating an instance: let's execute it.
334
        return ((Script) goo).run();
×
335
      }
336
      else {
337
        // An instance of the scripted class: let's return it as-is.
338
        return goo;
2✔
339
      }
340
    }
341
    catch (ConstructorNotFoundException ex) {
×
342
      throw new ScriptCompilationException(
×
343
              "No default constructor on Groovy script class: " + scriptClass.getName(), ex);
×
344
    }
345
    catch (InstantiationException ex) {
×
346
      throw new ScriptCompilationException(
×
347
              scriptSource, "Unable to instantiate Groovy script class: " + scriptClass.getName(), ex);
×
348
    }
349
    catch (IllegalAccessException | InaccessibleObjectException ex) {
×
350
      throw new ScriptCompilationException(
×
351
              scriptSource, "Could not access Groovy script constructor: " + scriptClass.getName(), ex);
×
352
    }
353
    catch (InvocationTargetException ex) {
×
354
      throw new ScriptCompilationException(
×
355
              "Failed to invoke Groovy script constructor: " + scriptClass.getName(), ex.getTargetException());
×
356
    }
357
  }
358

359
  @Override
360
  public String toString() {
361
    return "GroovyScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
×
362
  }
363

364
  /**
365
   * Wrapper that holds a temporarily cached result object.
366
   */
367
  private record CachedResultHolder(@Nullable Object object) {
×
368

369
  }
370

371
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc