In my previous post I was talking about how to create a service using Jdk service provider. In current post I am going to talk about how to implement your own Jsr223 Script Engine. And as you will see, script engines are a kind of Jdk service providers, so instead of creating your own set of interfaces, we are going to see how to implement a service provider of an already defined service.
Moreover we are going to see how we can convert a scripting language expression evaluator (in this case Spring EL) to be Jsr-223 compliant.
First of all for those who do not know what is Jsr-223 specification (or Java Scripting API), Jsr-223 is a scripting language independent framework for using script engines from Java code. Thanks of Jsr-223, we only use javax.script package instead of importing language specific classes.
For example using Spring EL scripting language, requires you import in your business class at least two Spring classes ExpressionParser and SpelExpressionParser, and if you want to use another scripting language, more specific vendor classes should be added, so your code is tied to script parser used.
If you use Java Scripting API, two classes are required, ScriptEngineFactory and ScriptEngine, regardless of the language used. And this occurs because ScriptEngine is an interface returned by ScriptEngineFactory service.
Let's start coding:
First class to implement is ScriptEngineFactory and is responsible of creating a new ScriptEngine. It must implement ScriptEngineFactory interface and its implementation will be the service provider of Scripting API.
First part of class is where we are defining what is the name of script language used to get the engine, the version of language (I have chosen Spring version), and version of engine.
Then three special methods:
Hope you found this post useful.
Feel free to use this code, I have created a git repository so you can download, branch, ... https://github.com/maggandalf/spEL223
Also I have uploaded project as zip file. Download Code.
Music: http://www.youtube.com/watch?v=UAOxCqSxRD0
Let's start coding:
First class to implement is ScriptEngineFactory and is responsible of creating a new ScriptEngine. It must implement ScriptEngineFactory interface and its implementation will be the service provider of Scripting API.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SpelScriptEngineFactory implements ScriptEngineFactory { | |
private static final String VERSION = "1.0"; | |
private static final String SHORT_NAME = "spEL"; | |
private static final String LANGUAGE_NAME = "spEL"; | |
private static final String LANGUAGE_VERSION = "3.0.x"; | |
@Override | |
public String getEngineName() { | |
return "Spring EL Scripting Language"; | |
} | |
@Override | |
public String getEngineVersion() { | |
return VERSION; | |
} | |
@Override | |
public List<String> getExtensions() { | |
return EXTENSIONS; | |
} | |
@Override | |
public List<String> getMimeTypes() { | |
return MIME_TYPES; | |
} | |
@Override | |
public List<String> getNames() { | |
return NAMES; | |
} | |
@Override | |
public String getLanguageName() { | |
return LANGUAGE_NAME; | |
} | |
@Override | |
public String getLanguageVersion() { | |
return LANGUAGE_VERSION; | |
} | |
@Override | |
public Object getParameter(String key) { | |
if (ScriptEngine.NAME.equals(key)) { | |
return SHORT_NAME; | |
} else if (ScriptEngine.ENGINE.equals(key)) { | |
return getEngineName(); | |
} else if (ScriptEngine.ENGINE_VERSION.equals(key)) { | |
return VERSION; | |
} else if (ScriptEngine.LANGUAGE.equals(key)) { | |
return LANGUAGE_NAME; | |
} else if (ScriptEngine.LANGUAGE_VERSION.equals(key)) { | |
return LANGUAGE_VERSION; | |
} else if ("THREADING".equals(key)) { | |
return "MULTITHREADED"; | |
} else { | |
throw new IllegalArgumentException("Invalid key"); | |
} | |
} | |
@Override | |
public String getMethodCallSyntax(String obj, String method, String... args) { | |
String ret = "T(" + obj.getClass() + ")." + method + "("; | |
int len = args.length; | |
if (len == 0) { | |
ret += ")"; | |
return ret; | |
} | |
for (int i = 0; i < len; i++) { | |
ret += args[i]; | |
if (i != len - 1) { | |
ret += ","; | |
} else { | |
ret += ")"; | |
} | |
} | |
return ret; | |
} | |
@Override | |
public String getOutputStatement(String toDisplay) { | |
throw new UnsupportedOperationException(); | |
} | |
@Override | |
public String getProgram(String... statements) { | |
throw new UnsupportedOperationException(); | |
} | |
@Override | |
public ScriptEngine getScriptEngine() { | |
return new SpelScriptEngineImpl(); | |
} | |
private static final List<String> NAMES; | |
private static final List<String> EXTENSIONS; | |
private static final List<String> MIME_TYPES; | |
static { | |
List<String> n = new ArrayList(2); | |
n.add(SHORT_NAME); | |
n.add(LANGUAGE_NAME); | |
NAMES = Collections.unmodifiableList(n); | |
n = new ArrayList<String>(1); | |
n.add("spEL"); | |
EXTENSIONS = Collections.unmodifiableList(n); | |
n = new ArrayList<String>(1); | |
n.add("application/x-spEL"); | |
MIME_TYPES = Collections.unmodifiableList(n); | |
} | |
} |
First part of class is where we are defining what is the name of script language used to get the engine, the version of language (I have chosen Spring version), and version of engine.
Then three special methods:
- getMethodCallSyntax: returns a String which can be used to invoke a method of a Java object using the syntax of the supported scripting language. In case spEL, the equivalent of calling a Java method from script, is using Types with special T operator.
- getOutputStream: returns a String that can be used as a statement to display the specified String using the syntax of the supported scripting language. In case of spEL, there is no output stream so an unsupported exception is thrown.
- getProgram: returns a valid scripting language executable progam with given statements. As previous method, an unsupported exception is thrown.
and finally the most important method getScriptEngine, which must return an implementation of ScriptEngine interface. In this case the Spring EL script engine. Let's see how spEL ScriptEngine is implemented.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SpelScriptEngineImpl extends AbstractScriptEngine implements | |
Compilable { | |
public static final String ROOT_OBJECT = "root"; | |
private volatile SpelScriptEngineFactory factory; | |
@Override | |
public Object eval(String script, ScriptContext context) | |
throws ScriptException { | |
Expression expression = parse(script); | |
return evalExpression(expression, context); | |
} | |
private Expression parse(String script) { | |
ExpressionParser expressionParser = getExpressionParser(); | |
return expressionParser.parseExpression(script); | |
} | |
private Object evalExpression(Expression expression, | |
ScriptContext scriptContext) { | |
StandardEvaluationContext standardEvaluationContext = getStandardEvaluationContext(scriptContext); | |
return expression.getValue(standardEvaluationContext); | |
} | |
private StandardEvaluationContext getStandardEvaluationContext( | |
ScriptContext scriptContext) { | |
StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext( | |
scriptContext.getAttribute(ROOT_OBJECT)); | |
standardEvaluationContext.setVariables(getVariables(scriptContext)); | |
return standardEvaluationContext; | |
} | |
private Map<String, Object> getVariables(ScriptContext scriptContext) { | |
Map<String, Object> variables = new HashMap<String, Object>(); | |
if (scriptContext.getBindings(ScriptContext.GLOBAL_SCOPE) != null) { | |
variables.putAll(scriptContext | |
.getBindings(ScriptContext.GLOBAL_SCOPE)); | |
} | |
if (scriptContext.getBindings(ScriptContext.ENGINE_SCOPE) != null) { | |
variables.putAll(scriptContext | |
.getBindings(ScriptContext.ENGINE_SCOPE)); | |
} | |
return variables; | |
} | |
private ExpressionParser getExpressionParser() { | |
return new SpelExpressionParser(); | |
} | |
@Override | |
public Object eval(Reader reader, ScriptContext context) | |
throws ScriptException { | |
return eval(readFully(reader), context); | |
} | |
@Override | |
public Bindings createBindings() { | |
return new SimpleBindings(); | |
} | |
@Override | |
public ScriptEngineFactory getFactory() { | |
if (factory == null) { | |
synchronized (this) { | |
if (factory == null) { | |
factory = new SpelScriptEngineFactory(); | |
} | |
} | |
} | |
return factory; | |
} | |
private class SpelCompiledScript extends CompiledScript { | |
private final Expression expression; | |
public SpelCompiledScript(Expression expression) { | |
this.expression = expression; | |
} | |
@Override | |
public Object eval(ScriptContext context) throws ScriptException { | |
return evalExpression(expression, context); | |
} | |
@Override | |
public ScriptEngine getEngine() { | |
return SpelScriptEngineImpl.this; | |
} | |
} | |
@Override | |
public CompiledScript compile(String script) throws ScriptException { | |
return new SpelCompiledScript(parse(script)); | |
} | |
@Override | |
public CompiledScript compile(Reader script) throws ScriptException { | |
return compile(readFully(script)); | |
} | |
} |
First of all class extends from AbstractScriptEngine, which provides implementation for several of the variants of the eval method. Compilable interface is implemented too. ScriptEngines that implements this interface can compile scripts so any further execution of expression does not require any recompilation.
Most important method is eval which should cause immediate script execution using vendor library.
Main purpose of this class is adapt Jsr-223 classes to Spring EL classes. If you look carefully this class, it contains a private inner class that extends CompiledScript. This class is responsible of storing compiled expression, so subsequent calls of eval method requires no reparsing. This class is returned when compile method is called.
As final note spEL language contains some features that go beyond a standard script like registering Java functions into script, referencing Spring Beans, expression templating or navigation through properties of root objects. Of all of these features I think it would be useful having root objects support in Jsr-223 implementation, so I have coded a special attribute named "root". So if you set an attribute to script context with name "root", attribute's content will be treated as root object.
Before implementing this solution I wondered another possibilities like not implementing root objects, or using Adapter pattern implementing two interfaces ScriptContext and EvaluationContext, but I dismissed because there were duplicated methods (different signature, same behaviour), and from developer's point of view could be a bit confusing. Moreover this approach implies that caller should know that are running spEL script. Also I planned of using MethodResolver or PropertyResolver but was rejected because spEL would have been extended.
With attribute approach, if you are not using root objects, there are no differences between using Scripting API with Javascript, Groovy or spEL.
And finally service configuration. Create META-INF/services directory into resources folder, and create javax.script.ScriptEngineFactory file; this is the full qualified name of service interface with next content:
Most important method is eval which should cause immediate script execution using vendor library.
Main purpose of this class is adapt Jsr-223 classes to Spring EL classes. If you look carefully this class, it contains a private inner class that extends CompiledScript. This class is responsible of storing compiled expression, so subsequent calls of eval method requires no reparsing. This class is returned when compile method is called.
As final note spEL language contains some features that go beyond a standard script like registering Java functions into script, referencing Spring Beans, expression templating or navigation through properties of root objects. Of all of these features I think it would be useful having root objects support in Jsr-223 implementation, so I have coded a special attribute named "root". So if you set an attribute to script context with name "root", attribute's content will be treated as root object.
Before implementing this solution I wondered another possibilities like not implementing root objects, or using Adapter pattern implementing two interfaces ScriptContext and EvaluationContext, but I dismissed because there were duplicated methods (different signature, same behaviour), and from developer's point of view could be a bit confusing. Moreover this approach implies that caller should know that are running spEL script. Also I planned of using MethodResolver or PropertyResolver but was rejected because spEL would have been extended.
With attribute approach, if you are not using root objects, there are no differences between using Scripting API with Javascript, Groovy or spEL.
And finally service configuration. Create META-INF/services directory into resources folder, and create javax.script.ScriptEngineFactory file; this is the full qualified name of service interface with next content:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
org.alexsotob.spel223.SpelScriptEngineFactory |
And that's all about implementation, with next unit tests you will see clearly how is used:
First one is testing that SpelScriptEngine is returned by ScriptEngineManager.
First one is testing that SpelScriptEngine is returned by ScriptEngineManager.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ScriptEngineManagerBehaviour { | |
@Test | |
public void shouldReturnSpelScriptEngine() { | |
ScriptEngineManager factory = new ScriptEngineManager(); | |
ScriptEngine scriptEngine = factory.getEngineByName("spEL"); | |
assertThat(scriptEngine, instanceOf(SpelScriptEngineImpl.class)); | |
} | |
} |
Not complicated, it is used as you would use for retrieving Javascript engine. Short name is used as engine identifier.
And next unit test contains some assertions about how some Spring EL features are used with ScriptEngine rather than SpelExpressionParser.
Notice how in first test, SpelScriptEngineImpl.ROOT_OBJECT constant is used, so Inventor instance is specified as root object.And next unit test contains some assertions about how some Spring EL features are used with ScriptEngine rather than SpelExpressionParser.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SpelScriptEngineBehaviour { | |
@Test | |
public void shouldNavigateThroughProperties() throws ScriptException { | |
SpelScriptEngineImpl scriptEngineImpl = new SpelScriptEngineImpl(); | |
ScriptContext scriptContext = new SimpleScriptContext(); | |
scriptContext.setAttribute(SpelScriptEngineImpl.ROOT_OBJECT, | |
new Inventor("Mike Tesla"), ScriptContext.ENGINE_SCOPE); | |
String name = (String) scriptEngineImpl.eval("#root.name", | |
scriptContext); | |
assertThat(name, is("Mike Tesla")); | |
} | |
@Test | |
public void shouldExecuteLogicalOperations() throws ScriptException { | |
SpelScriptEngineImpl scriptEngineImpl = new SpelScriptEngineImpl(); | |
Boolean trueValue = (Boolean) scriptEngineImpl.eval("true or false"); | |
assertThat(trueValue, is(true)); | |
} | |
@Test | |
public void shouldExecuteMathematicalOperations() throws ScriptException { | |
SpelScriptEngineImpl scriptEngineImpl = new SpelScriptEngineImpl(); | |
Integer two = (Integer) scriptEngineImpl.eval("1+1"); | |
assertThat(two, is(2)); | |
} | |
@Test | |
public void shouldEvalVariables() throws ScriptException { | |
SpelScriptEngineImpl scriptEngineImpl = new SpelScriptEngineImpl(); | |
ScriptContext scriptContext = new SimpleScriptContext(); | |
scriptContext.setAttribute("name", "Mike Tesla", | |
ScriptContext.ENGINE_SCOPE); | |
String name = (String) scriptEngineImpl.eval("#name", scriptContext); | |
assertThat(name, is("Mike Tesla")); | |
} | |
@Test | |
public void shouldUseTypes() throws ScriptException { | |
SpelScriptEngineImpl scriptEngineImpl = new SpelScriptEngineImpl(); | |
Calendar calendar = (Calendar) scriptEngineImpl | |
.eval("T(java.util.Calendar).getInstance()"); | |
assertThat(calendar, instanceOf(Calendar.class)); | |
} | |
private static class Inventor { | |
private String name; | |
public Inventor(String inventor) { | |
this.name = inventor; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
} | |
} |
Hope you found this post useful.
Feel free to use this code, I have created a git repository so you can download, branch, ... https://github.com/maggandalf/spEL223
Also I have uploaded project as zip file. Download Code.
Music: http://www.youtube.com/watch?v=UAOxCqSxRD0
2 comentarios:
Nice read. Bad example of singleton pattern, though:
@Override
public ScriptEngineFactory getFactory() {
if (factory == null) {
synchronized (this) {
if (factory == null) {
factory = new SpelScriptEngineFactory();
}
}
}
return factory;
}
The second if-statement will be optimized away, since the optimizer only checks the current thread and does not take multi-threading into account.
Thank you very much for reading my blog, I partially agree with your comment. Thanks of you I have seen the error.
This is not the best strategy for implementing singleton pattern; I have an Eclipse plugin that creates automatically patterns, the problem was that when I wrote this example I used an old version of this plugin, newer versions resolves this singleton pattern problem. But see that post http://en.wikipedia.org/wiki/Double-checked_locking and you will see that implementing Singleton pattern is not as easy as one can think.
Anyway thank you very much again because you have made me see that I installed the wrong version of plugin.
Publicar un comentario