0
votes

I'm working on a java spring app which I need to provide the ability to add new code without altering standard code.

My challenges:

  1. Store "custom" code in a database
  2. Inject code at execution time into existing standard code
  3. Isolate custom code so that two users do not execute each others code.

In other words, what I am looking for is upon method execution, look to the db if user has custom code for this method, if no, execute standard code; if yes, inject code, execute modified method then dispose of custom code.

I've looked at AspectJ and Javassist. I have a process working with AspectJ but I can only get it to work if the custom code is in it's own jar and added at build/runtime. I haven't spent much time with Javassist but from the docs, it looks like i'm in the same boat.

Here's an extremely simplified example of what i'm looking to do.

@RestController
@RequestMapping("/test")
public class TestController {
   @RequestMapping(value = "/test", method = RequestMethod.GET)
   public String test() {

      String results = "";

      results = "foo";

      //<-- insert custom code here (if exists) each time method is invoked

      results += "bar";

      return results;
   }
}
1
Why do you need this? As this sounds like very bad idea. But if you need something dynamic like that you should look at java scripting engine, there is API that allows you to execute dynamically code in "any" language - as long as you will find implementation for it. And you can easily find one for JavaScript, Groovy, Kotlin, Ruby. - GotoFinal
And remember that such injected code (including scripts from my comment above) is always unsafe, someone could use that code to crash application or extract any other code from database or just send database to any place they want and get all the data. - GotoFinal
All valid points and does indeed sound like a horrible idea. All that considered, if I was in a position to start all over, I would use a dynamic language and call it a day. The short of it is I'm working with a cloud hosted application and need to provide capabilities for users to extend functionality. - felix53
So you are about to change your application in order to add that functionality. Fine. Then you are also in a position to do that via scripting engine. Store the code in a database and execute it via scripting interface. No need to "start all over". - kriegaex

1 Answers

2
votes

As for Java scripting support according to JSR-223, here is some introductory documentation for you.

By default the Nashorn JavaScript engine is contained in the JRE, but since Java 11 you get a warning that it is deprecated and will be removed in the future. This is not a big problem because you will still be able to use it and there are also other JSR-223-compatible scripting languages available.

This example shows how to use

  • JavaScript,
  • Groovy and
  • Lua

scripts from Java. Please make sure that you add the corresponding

libraries to your classpath if you want to use the additional two languages and not just the built-in JS engine.

The scripts in each language demonstrate the same basics things, i.e. how to

  • call a method on a Java object (File.getCanonicalPath()) from the script,
  • how to convert a string to upper case,
  • how to repeat a string (in JS we have to define our own prototype method for that because Nashorn does not implement the repeat(n) method by itself).
package de.scrum_master.app;

import java.io.File;
import javax.script.*;

public class Application {
  private static final ScriptEngineManager MANAGER = new ScriptEngineManager();

  public static void main(String[] args) throws ScriptException {
    listScriptingEngines();
    System.out.println(runScript("JavaScript", "print(file.getCanonicalPath()); String.prototype.repeat = function(num) { return new Array(num + 1).join(this) }; (text.toUpperCase() + '_').repeat(2)"));
    System.out.println(runScript("Groovy", "println file.canonicalPath; (text.toUpperCase() + '_') * 2"));
    System.out.println(runScript("lua", "print(file:getCanonicalPath()); return string.rep(string.upper(text) .. '_', 2)"));
  }

  public static void listScriptingEngines() {
    for (ScriptEngineFactory factory : MANAGER.getEngineFactories()) {
      System.out.printf("Script Engine: %s (%s)%n", factory.getEngineName(), factory.getEngineVersion());
      System.out.printf("  Language: %s (%s)%n", factory.getLanguageName(), factory.getLanguageVersion());
      factory.getNames().stream().forEach(name -> System.out.printf("    Alias: %s%n", name));
    }
  }

  public static String runScript(String language, String script) throws ScriptException {
    ScriptEngine engine = MANAGER.getEngineByName(language);
    // Expose Java objects as global variables to script engine
    engine.put("file", new File("/my/" + language + "/test.txt"));
    engine.put("text", language);
    // Use script result for generating return value
    return engine.eval(script) + "suffix";
  }
}

If you run the program with groovy-all on the classpath, the output should look like this:

Script Engine: Groovy Scripting Engine (2.0)
  Language: Groovy (2.4.13)
    Alias: groovy
    Alias: Groovy
Script Engine: Luaj (Luaj-jse 3.0.1)
  Language: lua (5.2)
    Alias: lua
    Alias: luaj
Script Engine: Oracle Nashorn (1.8.0_211)
  Language: ECMAScript (ECMA - 262 Edition 5.1)
    Alias: nashorn
    Alias: Nashorn
    Alias: js
    Alias: JS
    Alias: JavaScript
    Alias: javascript
    Alias: ECMAScript
    Alias: ecmascript
C:\my\JavaScript\test.txt
JAVASCRIPT_JAVASCRIPT_suffix
C:\my\Groovy\test.txt
GROOVY_GROOVY_suffix
C:\my\lua\test.txt
LUA_LUA_suffix

Of course in your own code you should replace the inline JS, Groovy or Lua code with something user-specific loaded from your code snippet database. You should also think about which variables you want to expose to the scripting engine and document it for your users.