0

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;
   }
}
kriegaex
  • 63,017
  • 15
  • 111
  • 202
felix53
  • 41
  • 5
  • 5
    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 Aug 08 '19 at 22:11
  • 4
    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 Aug 08 '19 at 22:12
  • 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 Aug 08 '19 at 22:22
  • 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 Aug 09 '19 at 03:01

1 Answers1

2

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.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Update: Now there are scripting examples in JavaScript, Groovy and Lua. – kriegaex Aug 09 '19 at 09:28
  • This is great, thanks for going the extra mile and including examples. I was aware of this capability with groovy but not javascript/lua. I still go back to my original challenge though and injecting the code into a method during execution time. As far as i can find with the research i've done, AspectJ and Javassist both do at compile or runtime and load via classloader. My backup plan is to add empty method references upon request but that will get out of hand very quickly which is why i was looking for a way to manipulate the bytecode when the method is actucally being executed – felix53 Aug 09 '19 at 12:27
  • 1
    @felix53 but why? why you want to do this in such weird way? it will be just more error prone, slower, harder to implement. Can you explain what is your goal? – GotoFinal Aug 09 '19 at 13:13
  • Goal is to allow users of a multi tenant hosted app to add functionality of backend logic. For example, there is a method that calculates rpms of a shaft given a motor as input. The user wants to add a step down gear as part of the calculation. Their logic must be placed at a specific line in the existing method for the calculation to return a correct result. It’s a legacy app and will be ugly. If there is no way to manipulate byte code during execution time, I’m fine with adding stubs but that will slow down the end user development because they will have to wait until the stub exists. – felix53 Aug 12 '19 at 14:45
  • All of this is possible via scripting hooks, nothing of it justifies bad software design because what you intend to implement now needs to be maintained in the future, maybe not by you. Besides, letting user-created code directly manipulate your byte code is like an invitation to hack your application. As for AOP, it is meant to help you implement cross-cutting concerns in a modular way, not for adding user-specific script code. As much as I love AspectJ, it is not the right tool for your purpose here. – kriegaex Aug 15 '19 at 01:25