Like spigot/bukkit plugins id like to be able to load jars in a file and load their classes. Ive managed to get this working with java class loader but the class has to extend a runnable for it to work. Id like to have my own custom interface to implement for each plugin(jar). So I could have functions that get ran on plugin load and so on. If anyone knows how to do this please let me know.
Plugin structure
A plugin is a .jar file. The plugin has a plugin.properties file with the properties of the plugin.
It looks like this:
plugin.main=com.example.plugins.ExamplePlugin
plugin.name=Example Plugin
plugin.description=Test 123
plugin.version=1.0
The file contains the main class, the plugin name, a description and the version.
A plugin must have a class that inherits from the abstract plugin class. This counts as the main class.
Code structure
Let's begin with the Plugin-class:
package com.example.plugins;
public abstract class Plugin {
protected PluginProperty property;
public abstract void onEnable();
public abstract void onDisable();
public PluginProperty getProperty() {
return property;
}
public void setProperty(PluginProperty property) {
this.property = property;
}
}
As you may see, I have chosen an abstract class here.
The class consists of two abstract methods (onEnable and onDisable). The plugin also has a PluginProperty object. The equivalent of this class in Spigot would be JavaPlugin.
Let's take a look at the PluginProperty class.
package com.example.plugins;
public class PluginProperty {
private String main;
private String name;
private String description;
private double version;
public PluginProperty(String main, String name, String description, double version) {
this.main = main;
this.name = name;
this.description = description;
this.version = version;
}
public String getMain() {
return main;
}
public void setMain(String main) {
this.main = main;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public double getVersion() {
return version;
}
public void setVersion(double version) {
this.version = version;
}
}
This class has all the necessary properties of a plugin in it. Most of the things here are self-explanatory, but I would still like to discuss main.
The string holds the name of the main plugin class of the plugin. This is basically the same as the main in the plugin.yml in Spigot.
The loader
Here is the PluginLoader-class:
package com.example.plugins;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Properties;
import java.util.zip.ZipException;
public class PluginLoader {
private static PluginProperty loadPluginProperties(File file) throws ZipException, IOException {
URL url = file.toURI().toURL();
String jarURL = "jar:" + url +"!/plugin.properties";
InputStream input;
URL inputURL = new URL(jarURL);
JarURLConnection conn = (JarURLConnection)inputURL.openConnection();
input = conn.getInputStream();
Properties property = new Properties();
property.load(input);
String main = property.getProperty("plugin.main");
String name = property.getProperty("plugin.name");
String description = property.getProperty("description");
double version = Double.parseDouble(property.getProperty("plugin.version"));
return new PluginProperty(main, name, description, version);
}
public static Plugin loadPlugin(File file) throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
if(!file.exists()) {
return null;
}
PluginProperty property = loadPluginProperties(file);
URL url = file.toURI().toURL();
String jarURL = "jar:" + url + "!/";
URL urls[] = {new URL(jarURL)};
URLClassLoader ucl = new URLClassLoader(urls);
Plugin plugin = (Plugin) Class.forName(property.getMain(), true, ucl).getDeclaredConstructor().newInstance();
plugin.setProperty(property);
return plugin;
}
}
The private loadPluginProperties method loads the plugin properties and returns the required object. The loadPlugin method loads the main class specified in the properties into an object and returns it.
Examples
I just gave you the basic framework for the plugin system. But how should you use it? Let's start with an example loader.
Here is the Main-class:
package com.example.plugins;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static List<Plugin> plugins = new ArrayList<Plugin>();
public static void main(String[] args) {
File[] pluginFiles = new File("plugins").listFiles();
//Load plugins
for(File f : pluginFiles) {
if(f.isDirectory()) {
continue;
}
if(!f.getName().endsWith(".jar")) {
continue;
}
Plugin p = null;
try {
p = PluginLoader.loadPlugin(f);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | IOException e) {
System.err.println("Failed to load plugin!");
e.printStackTrace();
}
Main.plugins.add(p);
}
//Enable plugins
for(Plugin p : plugins) {
p.onEnable();
}
//Disable plugins
for(Plugin p : plugins) {
p.onDisable();
}
}
}
I won't go into much detail here as I think it's pretty self-explanatory. If you have a question, just ask me through the comments.
After exporting the previously written as a JAR, add it to the classpath in a new project. Don't forget to create a plugin.properties file.
This is an example plugin that is compatible with the .properties file specified above:
package com.example.plugins;
public class ExamplePlugin extends Plugin {
#Override
public void onEnable() {
System.out.println("Hello world!");
}
#Override
public void onDisable() {
}
}
When I export this plugin and put it in the plugins folder I get the following output:
Hello world!
The End
It would be recommended to use JSON or YAML, XML etc. instead of the built-in Java property files. This is the basic structure for plugins. Have fun!
Related
Here is the project architecture:
Here is the Car class that won't compile:
package car;
import engine.Engine;
public class Car {
private Engine engine;
/***
* Constructor
* #param engine The engine that the car object will have
*/
Car(Engine engine) {
engine = new Engine(engine); // Use the Engine copy constructor
}
}
Here is the error:
Here is the Engine class in the engine package:
package engine;
public class Engine {
private String name;
private int displacement;
public Engine(String name, int displacement) throws Exception {
setName(name);
setDisplacement(displacement);
}
public String getName() {return name;}
public int getDisplacement() {return displacement;}
private void setName(String name) throws Exception {
if (name.trim().length() == 0) {
throw new Exception("Engine name cannot be blank");
}
}
private void setDisplacement(int displacement) throws Exception {
if (displacement < 0) {
throw new Exception("Engine displacement cannot be zero or negative");
}
}
}
Try cleaning the project in Project --> clean
Fixed: I didn't change the code, I deleted the project from the workspace, closed Eclipse, reopened Eclipse with a different workspace, and imported the same project into the workspace. Arrrgh.
My code is exactly the same as the example shown on py4j website:
Implementing Java Interfaces from Python
Except my classes are all in the same src.main.java package
(see below for the code)
Problem:
If I do a gradle fatjar build with ListenerApplication as main, then execute the jar, everything works fine. If I do a gradle fatjar build and instead access the code via a plugin interface, I get the following error:
Py4JError: An error occurred while calling o0.registerListener. Trace:
py4j.Py4JException: Invalid interface name: ExampleListener
at py4j.Protocol.getPythonProxy(Protocol.java:429)
at py4j.Protocol.getObject(Protocol.java:311)
at py4j.commands.AbstractCommand.getArguments(AbstractCommand.java:82)
at py4j.commands.CallCommand.execute(CallCommand.java:77)
at py4j.GatewayConnection.run(GatewayConnection.java:238)
at java.lang.Thread.run(Thread.java:748)
Question: Why does Py4J have problems finding "ExampleListener" when the .jar is run as a plugin and not as an application? I can even add :
public String classtest() throws Exception {
System.out.println("classtest called");
Class<?> py = Class.forName("ExampleListener");
return py.toString();
}
to the ListenerApplication, which will return the correct interface both when run as plugin and as application! The interesting thing is, if I run the program plus plugin from netbeans IDE, everything works fine! Does Netbeans somehow expose the interface, while the application run directly, does not?
Plugin interface
import org.micromanager.MenuPlugin;
import org.micromanager.Studio;
import org.scijava.plugin.Plugin;
import org.scijava.plugin.SciJavaPlugin;
import py4j.GatewayServer;
#Plugin(type = MenuPlugin.class)
public class Py4JPluginInterface implements MenuPlugin, SciJavaPlugin{
private static final String menuName = "Simpletest_gradle";
private static final String tooltipDescription = "py4j gateway";
private static final String version = "0.1";
private static final String copyright = "copyright";
#Override
public String getSubMenu() {
return "Simpletest_gradle";
}
#Override
public void onPluginSelected() {
GatewayServer gatewayServer = new GatewayServer(new ListenerApplication());
gatewayServer.start();
System.out.println("Gateway Started at IP:port = "+gatewayServer.getAddress()+":"+gatewayServer.getPort());
}
#Override
public void setContext(Studio app) {
}
#Override
public String getName() {
return menuName;
}
#Override
public String getHelpText() {
return tooltipDescription;
}
#Override
public String getVersion() {
return version;
}
#Override
public String getCopyright() {
return copyright;
}
}
The interface:
//py4j/examples/ExampleListener.java
package py4j.examples;
public interface ExampleListener {
Object notify(Object source);
}
The application:
package py4j.examples;
import py4j.GatewayServer;
import java.util.ArrayList;
import java.util.List;
public class ListenerApplication {
List<ExampleListener> listeners = new ArrayList<ExampleListener>();
public void registerListener(ExampleListener listener) {
listeners.add(listener);
}
public void notifyAllListeners() {
for (ExampleListener listener: listeners) {
Object returnValue = listener.notify(this);
System.out.println(returnValue);
}
}
#Override
public String toString() {
return "<ListenerApplication> instance";
}
public static void main(String[] args) {
ListenerApplication application = new ListenerApplication();
GatewayServer server = new GatewayServer(application);
server.start(true);
}
}
The python listener
from py4j.java_gateway import JavaGateway, CallbackServerParameters
class PythonListener(object):
def __init__(self, gateway):
self.gateway = gateway
def notify(self, obj):
print("Notified by Java")
print(obj)
gateway.jvm.System.out.println("Hello from python!")
return "A Return Value"
class Java:
implements = ["py4j.examples.ExampleListener"]
if __name__ == "__main__":
gateway = JavaGateway(
callback_server_parameters=CallbackServerParameters())
listener = PythonListener(gateway)
gateway.entry_point.registerListener(listener)
gateway.entry_point.notifyAllListeners()
gateway.shutdown()
For those who are interested, this was a class loader issue, which is apparently common for plugin/OSGI apps.
See the maintainer's response:
https://github.com/bartdag/py4j/issues/339#issuecomment-473655738
I simply added the following to the Java-side ListenerApplication constructor:
RootClassLoadingStrategy rmmClassLoader = new RootClassLoadingStrategy();
ReflectionUtil.setClassLoadingStrategy(rmmClassLoader);
I'm trying to use YamlBeans to serialize fxml properties. Specificaly a Property. The class has a private property field and the fxml standard getter and setter methods but the information is not saved to the file when serialization occurs.
Entry point:
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException{
Person person = new Person(5);
YamlSerializer.serialize(person, System.getProperty("user.dir") + "/person.yml");
}
}
Person.java
import javafx.beans.property.Property;
import javafx.beans.property.SimpleDoubleProperty;
public class Person{
private Property<Number> age;
public Person(){
age = new SimpleDoubleProperty();
age.setValue(3);
}
public Person(Number age){
this.age = new SimpleDoubleProperty(age.doubleValue());
}
public Property<Number> ageProperty() {
return this.age;
}
public Number getAge() {
return this.ageProperty().getValue();
}
public void setAge(final Number age) {
this.ageProperty().setValue(age);
}
}
YamlSerializer.java
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import com.esotericsoftware.yamlbeans.YamlReader;
import com.esotericsoftware.yamlbeans.YamlWriter;
public class YamlSerializer {
public static void serialize(Object object, String path) throws IOException{
File file = new File(path);
if(!file.exists())
file.getParentFile().mkdirs();
YamlWriter writer = new YamlWriter(new FileWriter(path));
writer.write(object);
writer.close();
}
public static Object deserialize(String path) throws IOException{
File file = new File(path);
if(!file.exists()){
if(!file.getParentFile().exists())
if (!file.getParentFile().mkdirs()){
System.out.println("Error creating files");
}
}
YamlReader reader = new YamlReader(new FileReader(path));
return reader.read();
}
}
Output file person.yml:
!Person {}
Your code looks OK. What does Beans getProperties return for your class? This is what YamlWriter uses.
Turns out Beans is finding all the properties, then looking for get/set methods. It looks like it should find your number field and then your setNumber and getNumber methods, but you didn't provide the full class code.
I suggest providing an SSCCE. Trying to help someone without that is a shot in the dark and a time waste.
Edit: The way Beans looks for fields, then a matching setter/getter, Property<Number> age doesn't work. Beans looks for a setter/getter of type Property<Number> and doesn't find them. IIRC it used to use Introspector, but that had to be ripped out because it was missing from Android.
To fix this you would need to patch Beans to be smarter about finding setter/getters without a corresponding field. A PR that does that would be merged.
I'm trying to get bytecode of cglib enhanced object this way using BCEL:
package app;
import cglib.MyInterceptor;
import net.sf.cglib.proxy.Enhancer;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import service.Tool;
public class CgLibApp {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
// target object
Tool tool = new Tool();
// proxying
Enhancer e = new Enhancer();
e.setSuperclass(tool.getClass());
e.setCallback(new MyInterceptor(tool));
Tool proxifiedTool = (Tool) e.create();
// trying to get proxy byte code
JavaClass clazz = Repository.lookupClass(proxifiedTool.getClass());
Method method = clazz.getMethod(Tool.class.getMethod("meth"));
System.out.println(method.getCode().toString());
}
}
But I'm getting:
Exception in thread "main" java.lang.ClassNotFoundException: SyntheticRepository could not load service.Tool$$EnhancerByCGLIB$$22a3afcc
at org.apache.bcel.util.SyntheticRepository.loadClass(SyntheticRepository.java:174)
at org.apache.bcel.util.SyntheticRepository.loadClass(SyntheticRepository.java:158)
at org.apache.bcel.Repository.lookupClass(Repository.java:74)
at app.CgLibApp.main(CgLibApp.java:21)
What should I do to get bytecode from Enhanced object?
BCEL queries a class loader for a .class file in order to get hold of the byte array that represents it. Such a class file does not exist for a dynamically generated class.
In order to get hold of the class file, you have to collect the byte code during the class file's creation. Cglib is built on top of ASM and it allows you to register your own ClassVisitors to collect a class file.
With the Enhancer, use the generateClass(ClassVisitor) method and hand the latter method a ClassWriter. After calling the method, you can get the byte code from the class writer object that you passed.
here is the sample code to print pseudo code of generated CGLIB class.
visitEnd method prints the generated class in text format.
package naga.cglib.demo;
import static org.objectweb.asm.Opcodes.ASM7;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.util.TraceClassVisitor;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.FixedValue;
public class App {
public static void main(String[] args) throws Exception, IllegalArgumentException, InvocationTargetException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new FixedValueImpl());
SampleClass proxy = (SampleClass) enhancer.create();
enhancer.generateClass(new CustomClassWriter());
System.out.println("Hello cglib!" + proxy.test(null));
}
}
class SampleClass {
public String test(String input) {
return "Hello world!";
}
}
class FixedValueImpl implements FixedValue {
#Override
public Object loadObject() throws Exception {
// TODO Auto-generated method stub
return "Hello cglib! from loadObject()";
}
}
class CustomClassWriter extends ClassVisitor {
TraceClassVisitor tracer;
PrintWriter pw = new PrintWriter(System.out);
public CustomClassWriter() {
super(ASM7);
tracer = new TraceClassVisitor(pw);
}
#Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("method name is :" + name);
return tracer.visitMethod(access, name, desc, signature, exceptions);
}
#Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
System.out.println("field name is :" + name);
return tracer.visitField(access, name, desc, signature, value);
}
public void visitEnd() {
tracer.visitEnd();
System.out.println(tracer.p.getText());
}
}
I've found this question while researching how to save the CGLIB-generated class in spring-boot 3.0 application (e.g. handling #Transactional or #Configuration-annotated classes). This simple approach may help:
import org.springframework.cglib.core.ReflectUtils;
...
public class SpringCglibUtils {
public static void initGeneratedClassHandler(String targetPath) {
File dir = new File(targetPath);
dir.mkdirs();
ReflectUtils.setGeneratedClassHandler((String className, byte[] classContent) -> {
try (FileOutputStream out = new FileOutputStream(new File(dir, className + ".class"))) {
out.write(classContent);
} catch (IOException e) {
throw new UncheckedIOException("Error while storing " + className, e);
}
});
}
}
and then define in your main class before creating context:
SpringCglibUtils.initGeneratedClassHandler("cglib");
Spring will store to the targetPath directory all generated class files.
Note: unfortunately it's not available before spring-boot 3
I got this code to instantiate a Life class from jar file:
import com.life.Life;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) {
try{
File file = new File("Life.jar");
URL url = file.toURI().toURL();
URL[] urls = new URL[]{url};
ClassLoader cl = new URLClassLoader(urls);
Class cls = cl.loadClass("com.life.Life");
Life life = (Life) cls.newInstance();
System.out.println("Message: "+life.getMessage());
}catch(Exception e){
e.printStackTrace();
}
}
}
Here's the content of the Life.jar
public class Life {
public String getMessage(){
return "Life is Beautiful!";
}
}
Here's my interface name Life
package com.life;
public interface Life {
public String getMessage();
}
The code above will throw an error:
java.lang.InstantiationException: com.life.Life
at java.lang.Class.newInstance0(Class.java:340)
at java.lang.Class.newInstance(Class.java:308)
at com.Main.main(Main.java:20)
BUILD SUCCESSFUL (total time: 0 seconds)
What's wrong with the code? How to resolve this?
This happened because your interface is also named Life ( java tried to instantiate an interface). Change public interface Life to public interface LifeInterface and then have your class Life implement that like :
public class Life implements LifeInterface
{
#Override
public String getMessage()
{
return "Life is Beautiful!";
}
}