Executing Uncompiled Groovy Scripts in a Grails Application

Print This Post Print This Post

In some web application projects a requirement often pops up that requires the execution of some scripts that may not have been included in the initial rollout or needs to be updated by hand on a live server. While this can be dangerous, it can be useful. In Java based web applications, it’s difficult to get some custom code executed without recompiling and restarting the app server, or even to inject some of the application or even request scoped objects.

There are a couple of options available for Grails, but I will go over the one that has seemed to work the best in my environments. I’ve removed some steps that are specific to my applications and have no real use here as well as generalized much of it and removed some error checking, so some additional knowledge may be required to implement it.

The Scripts

The scripts are contained in a single place, though with some work that can be extended. It’s generally easier to keep the scripts within the application deployment, but out of the reach of users. For this, I put them in /WEB-INF/scripts. You may want to verify the security settings on your application to may sure that the scripts are, in fact, non-accessible from the outside.
Each script is nothing more than a Groovy class with the same name as the file (e.g. ClassName.groovy). When compiled, it will have access to the applications classpath, so any classes or libraries needed to execution the script will be available.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import us.wthr.someapplication.util.*
import groovy.sql.Sql
 
class Sandbox {
 
	def log
	def grailsApplication
	def someOtherService
 
	def scriptMethod = { someVar ->
 
		log.info "Hello!"
		return "Thanks for passing ${someVar}"
	}
}

Notice the variables as they will be injected with their matching services on execution. In this example, three services (including the logger) are injected. A single method is available as a closure that simply dumps ‘Hello!’ to the log. Though this is a simplified example, the script has full access to do whatever it needs as if it were executed within a service (as it actually will be).

The Service

The only real component being added to the application is the script execution service. The service has the job of locating, compiling, and executing the script. It has the option to do further functions such as logging executions, session management or threading.

Skeleton Service Class

1
2
3
4
5
6
7
8
9
10
11
12
13
package us.wthr.someApplication.services
 
import org.codehaus.groovy.grails.commons.ApplicationHolder
import grails.util.Environment
import org.springframework.context.ApplicationContext;
import org.codehaus.groovy.grails.web.context.ServletContextHolder;
import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes;
 
class ScriptExecuteService {
 
	def grailsApplication
 
}

The Script Compiler

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
def getScriptClass(script)
{
    	def servletContext = ApplicationHolder.getApplication()
		.getParentContext().getServletContext()
 
	def realPath = servletContext
		.getRealPath("WEB-INF/scripts/${script}.groovy")
 
	def sourceFile = new File(realPath)
 
	if (sourceFile.exists()) {
		log.info "Loading script from ${realPath}"
		def groovySource = new GroovyCodeSource(sourceFile)
		GroovyClassLoader gcl = new 
			GroovyClassLoader(this.getClass().getClassLoader())
 
		def scriptClass = gcl.parseClass(groovySource)
		def classInstance = scriptClass.newInstance()
 
		def ctx = (ApplicationContext) ServletContextHolder
			.getServletContext()
			.getAttribute(GrailsApplicationAttributes.APPLICATION_CONTEXT);
 
 
		ctx.beanFactory.autowireBeanProperties(classInstance,
			ctx.beanFactory.AUTOWIRE_BY_NAME, false)
 
		try {
			classInstance.log = log
		} catch (ex) { }
		return classInstance
	} else {
		log.info "Script not found at ${realPath}"
		return null
	}	
}

This function in ScriptExecuteService creates a ready-to-run script class by following these main steps:

  1. Determine the real path on the filesystem for the script (line 6), then getting an instance of the File object. (line 9)
  2. Load the script as Groovy source (line 13)
  3. Get the Groovy classloader (line 14) and use it to generate the class and get an instance (lines 17-18)
  4. Get the application context bean and use it to auto-inject variables (lines 20-26)
  5. Since the ‘log’ object isn’t provided by the application context, manually add it (line 29). Wrap it in a try/catch block for cases where the script doesn’t have a ‘log’ variable.

The Execute Method

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
def executeScript(script, method) 
{
 
	def message = ""
	def success = false
	def details = "ok"
 
	def someVariable = "Blah Blah Yadda Yadda"
	def scriptClass = getScriptClass(script)
 
	if (scriptClass != null) {
 
		try {
			details = scriptClass[method].call(someVariable)
			success = true
		} catch (Exception ex) {
			success = false
			details = ex.getMessage()
			log.warn "Failed to execute '${script}/${method}'", ex
		}
 
		if (success) {
			message = "Script '${script}/${method}' completed"
		} else {
			message = "Script '${script}/${method}' failed"
		}
	} else {
		message = "Cannot execute. Script '${script}' not found."
	}
 
	return message
}

This is the service function called to execute a particular script and method (Allowing multiple methods per script). The example creates a simple variable and passes it to the script, which is somewhat useless, but included for demonstration purposes. The main steps followed are:

  1. Call getScriptClass(scriptname) to get the class instance (line 9)
  2. Execute the script method, save the result (line 14)

Conclusion

This may or may not be useful in your Grails application as described, but hopefully the concept will help. The application I am building in my job uses this in a couple of areas such as install migration (outside of Bootstrap) and other background services which have the potential to need edits without any downtime.

Leave a Reply