Add preview of automatic library support
Example plugin.yml usage: ``` libraries: - com.squareup.okhttp3:okhttp:4.9.0 ``` Libraries will only be accessible to plugins and their transitive depends, allowing for multiple versions of the same library to be used by different plugins. This is a preview feature. Feedback is welcome so that it may be refined before being made widely available.
This commit is contained in:
parent
8d783aa172
commit
6a039de8db
21
api/pom.xml
21
api/pom.xml
@ -49,6 +49,27 @@
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-resolver-provider</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<!-- not part of the API proper -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.resolver</groupId>
|
||||
<artifactId>maven-resolver-connector-basic</artifactId>
|
||||
<version>1.6.2</version>
|
||||
<!-- not part of the API proper -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.resolver</groupId>
|
||||
<artifactId>maven-resolver-transport-http</artifactId>
|
||||
<version>1.6.2</version>
|
||||
<!-- not part of the API proper -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
|
123
api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java
Normal file
123
api/src/main/java/net/md_5/bungee/api/plugin/LibraryLoader.java
Normal file
@ -0,0 +1,123 @@
|
||||
package net.md_5.bungee.api.plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
|
||||
import org.eclipse.aether.DefaultRepositorySystemSession;
|
||||
import org.eclipse.aether.RepositorySystem;
|
||||
import org.eclipse.aether.artifact.Artifact;
|
||||
import org.eclipse.aether.artifact.DefaultArtifact;
|
||||
import org.eclipse.aether.collection.CollectRequest;
|
||||
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
|
||||
import org.eclipse.aether.graph.Dependency;
|
||||
import org.eclipse.aether.impl.DefaultServiceLocator;
|
||||
import org.eclipse.aether.repository.LocalRepository;
|
||||
import org.eclipse.aether.repository.RemoteRepository;
|
||||
import org.eclipse.aether.repository.RepositoryPolicy;
|
||||
import org.eclipse.aether.resolution.ArtifactResult;
|
||||
import org.eclipse.aether.resolution.DependencyRequest;
|
||||
import org.eclipse.aether.resolution.DependencyResolutionException;
|
||||
import org.eclipse.aether.resolution.DependencyResult;
|
||||
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
|
||||
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
|
||||
import org.eclipse.aether.transfer.AbstractTransferListener;
|
||||
import org.eclipse.aether.transfer.TransferCancelledException;
|
||||
import org.eclipse.aether.transfer.TransferEvent;
|
||||
import org.eclipse.aether.transport.http.HttpTransporterFactory;
|
||||
|
||||
class LibraryLoader
|
||||
{
|
||||
|
||||
private final Logger logger;
|
||||
private final RepositorySystem repository;
|
||||
private final DefaultRepositorySystemSession session;
|
||||
private final List<RemoteRepository> repositories;
|
||||
|
||||
public LibraryLoader(Logger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
|
||||
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
|
||||
locator.addService( RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class );
|
||||
locator.addService( TransporterFactory.class, HttpTransporterFactory.class );
|
||||
|
||||
this.repository = locator.getService( RepositorySystem.class );
|
||||
this.session = MavenRepositorySystemUtils.newSession();
|
||||
|
||||
session.setChecksumPolicy( RepositoryPolicy.CHECKSUM_POLICY_FAIL );
|
||||
session.setLocalRepositoryManager( repository.newLocalRepositoryManager( session, new LocalRepository( "libraries" ) ) );
|
||||
session.setTransferListener( new AbstractTransferListener()
|
||||
{
|
||||
@Override
|
||||
public void transferStarted(TransferEvent event) throws TransferCancelledException
|
||||
{
|
||||
logger.log( Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName() );
|
||||
}
|
||||
} );
|
||||
session.setReadOnly();
|
||||
|
||||
this.repositories = repository.newResolutionRepositories( session, Arrays.asList( new RemoteRepository.Builder( "central", "default", "https://repo.maven.apache.org/maven2" ).build() ) );
|
||||
}
|
||||
|
||||
public ClassLoader createLoader(PluginDescription desc)
|
||||
{
|
||||
if ( desc.getLibraries().isEmpty() )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[]
|
||||
{
|
||||
desc.getName(), desc.getLibraries().size()
|
||||
} );
|
||||
|
||||
List<Dependency> dependencies = new ArrayList<>();
|
||||
for ( String library : desc.getLibraries() )
|
||||
{
|
||||
Artifact artifact = new DefaultArtifact( library );
|
||||
Dependency dependency = new Dependency( artifact, null );
|
||||
|
||||
dependencies.add( dependency );
|
||||
}
|
||||
|
||||
DependencyResult result;
|
||||
try
|
||||
{
|
||||
result = repository.resolveDependencies( session, new DependencyRequest( new CollectRequest( (Dependency) null, dependencies, repositories ), null ) );
|
||||
} catch ( DependencyResolutionException ex )
|
||||
{
|
||||
throw new RuntimeException( "Error resolving libraries", ex );
|
||||
}
|
||||
|
||||
List<URL> jarFiles = new ArrayList<>();
|
||||
for ( ArtifactResult artifact : result.getArtifactResults() )
|
||||
{
|
||||
File file = artifact.getArtifact().getFile();
|
||||
|
||||
URL url;
|
||||
try
|
||||
{
|
||||
url = file.toURI().toURL();
|
||||
} catch ( MalformedURLException ex )
|
||||
{
|
||||
throw new AssertionError( ex );
|
||||
}
|
||||
|
||||
jarFiles.add( url );
|
||||
logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[]
|
||||
{
|
||||
desc.getName(), file
|
||||
} );
|
||||
}
|
||||
|
||||
URLClassLoader loader = new URLClassLoader( jarFiles.toArray( new URL[ jarFiles.size() ] ) );
|
||||
|
||||
return loader;
|
||||
}
|
||||
}
|
@ -1,12 +1,23 @@
|
||||
package net.md_5.bungee.api.plugin;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.CodeSource;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import lombok.ToString;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
|
||||
@ToString(of = "desc")
|
||||
final class PluginClassloader extends URLClassLoader
|
||||
{
|
||||
|
||||
@ -14,6 +25,10 @@ final class PluginClassloader extends URLClassLoader
|
||||
//
|
||||
private final ProxyServer proxy;
|
||||
private final PluginDescription desc;
|
||||
private final JarFile jar;
|
||||
private final Manifest manifest;
|
||||
private final URL url;
|
||||
private final ClassLoader libraryLoader;
|
||||
//
|
||||
private Plugin plugin;
|
||||
|
||||
@ -22,11 +37,18 @@ final class PluginClassloader extends URLClassLoader
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
public PluginClassloader(ProxyServer proxy, PluginDescription desc, URL[] urls)
|
||||
public PluginClassloader(ProxyServer proxy, PluginDescription desc, File file, ClassLoader libraryLoader) throws IOException
|
||||
{
|
||||
super( urls );
|
||||
super( new URL[]
|
||||
{
|
||||
file.toURI().toURL()
|
||||
} );
|
||||
this.proxy = proxy;
|
||||
this.desc = desc;
|
||||
this.jar = new JarFile( file );
|
||||
this.manifest = jar.getManifest();
|
||||
this.url = file.toURI().toURL();
|
||||
this.libraryLoader = libraryLoader;
|
||||
|
||||
allLoaders.add( this );
|
||||
}
|
||||
@ -34,10 +56,10 @@ final class PluginClassloader extends URLClassLoader
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
|
||||
{
|
||||
return loadClass0( name, resolve, true );
|
||||
return loadClass0( name, resolve, true, true );
|
||||
}
|
||||
|
||||
private Class<?> loadClass0(String name, boolean resolve, boolean checkOther) throws ClassNotFoundException
|
||||
private Class<?> loadClass0(String name, boolean resolve, boolean checkOther, boolean checkLibraries) throws ClassNotFoundException
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -45,6 +67,17 @@ final class PluginClassloader extends URLClassLoader
|
||||
} catch ( ClassNotFoundException ex )
|
||||
{
|
||||
}
|
||||
|
||||
if ( checkLibraries && libraryLoader != null )
|
||||
{
|
||||
try
|
||||
{
|
||||
return libraryLoader.loadClass( name );
|
||||
} catch ( ClassNotFoundException ex )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if ( checkOther )
|
||||
{
|
||||
for ( PluginClassloader loader : allLoaders )
|
||||
@ -53,13 +86,66 @@ final class PluginClassloader extends URLClassLoader
|
||||
{
|
||||
try
|
||||
{
|
||||
return loader.loadClass0( name, resolve, false );
|
||||
return loader.loadClass0( name, resolve, false, proxy.getPluginManager().isTransitiveDepend( desc, loader.desc ) );
|
||||
} catch ( ClassNotFoundException ex )
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ClassNotFoundException( name );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException
|
||||
{
|
||||
String path = name.replace( '.', '/' ).concat( ".class" );
|
||||
JarEntry entry = jar.getJarEntry( path );
|
||||
|
||||
if ( entry != null )
|
||||
{
|
||||
byte[] classBytes;
|
||||
|
||||
try ( InputStream is = jar.getInputStream( entry ) )
|
||||
{
|
||||
classBytes = ByteStreams.toByteArray( is );
|
||||
} catch ( IOException ex )
|
||||
{
|
||||
throw new ClassNotFoundException( name, ex );
|
||||
}
|
||||
|
||||
int dot = name.lastIndexOf( '.' );
|
||||
if ( dot != -1 )
|
||||
{
|
||||
String pkgName = name.substring( 0, dot );
|
||||
if ( getPackage( pkgName ) == null )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( manifest != null )
|
||||
{
|
||||
definePackage( pkgName, manifest, url );
|
||||
} else
|
||||
{
|
||||
definePackage( pkgName, null, null, null, null, null, null, null );
|
||||
}
|
||||
} catch ( IllegalArgumentException ex )
|
||||
{
|
||||
if ( getPackage( pkgName ) == null )
|
||||
{
|
||||
throw new IllegalStateException( "Cannot find package " + pkgName );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CodeSigner[] signers = entry.getCodeSigners();
|
||||
CodeSource source = new CodeSource( url, signers );
|
||||
|
||||
return defineClass( name, classBytes, 0, classBytes.length, source );
|
||||
}
|
||||
|
||||
throw new ClassNotFoundException( name );
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@ package net.md_5.bungee.api.plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@ -48,4 +50,8 @@ public class PluginDescription
|
||||
* Optional description.
|
||||
*/
|
||||
private String description = null;
|
||||
/**
|
||||
* Optional libraries.
|
||||
*/
|
||||
private List<String> libraries = new LinkedList<>();
|
||||
}
|
||||
|
@ -4,10 +4,12 @@ import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
import com.google.common.graph.Graphs;
|
||||
import com.google.common.graph.MutableGraph;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@ -49,6 +51,8 @@ public final class PluginManager
|
||||
private final Yaml yaml;
|
||||
private final EventBus eventBus;
|
||||
private final Map<String, Plugin> plugins = new LinkedHashMap<>();
|
||||
private final MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
|
||||
private final LibraryLoader libraryLoader;
|
||||
private final Map<String, Command> commandMap = new HashMap<>();
|
||||
private Map<String, PluginDescription> toLoad = new HashMap<>();
|
||||
private final Multimap<Plugin, Command> commandsByPlugin = ArrayListMultimap.create();
|
||||
@ -67,6 +71,17 @@ public final class PluginManager
|
||||
yaml = new Yaml( yamlConstructor );
|
||||
|
||||
eventBus = new EventBus( proxy.getLogger() );
|
||||
|
||||
LibraryLoader libraryLoader = null;
|
||||
try
|
||||
{
|
||||
libraryLoader = new LibraryLoader( proxy.getLogger() );
|
||||
} catch ( NoClassDefFoundError ex )
|
||||
{
|
||||
// Provided depends were not added back
|
||||
proxy.getLogger().warning( "Could not initialize LibraryLoader (missing dependencies?)" );
|
||||
}
|
||||
this.libraryLoader = libraryLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -309,6 +324,7 @@ public final class PluginManager
|
||||
status = false;
|
||||
}
|
||||
|
||||
dependencyGraph.putEdge( plugin.getName(), dependName );
|
||||
if ( !status )
|
||||
{
|
||||
break;
|
||||
@ -320,10 +336,7 @@ public final class PluginManager
|
||||
{
|
||||
try
|
||||
{
|
||||
URLClassLoader loader = new PluginClassloader( proxy, plugin, new URL[]
|
||||
{
|
||||
plugin.getFile().toURI().toURL()
|
||||
} );
|
||||
URLClassLoader loader = new PluginClassloader( proxy, plugin, plugin.getFile(), ( libraryLoader != null ) ? libraryLoader.createLoader( plugin ) : null );
|
||||
Class<?> main = loader.loadClass( plugin.getMain() );
|
||||
Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance();
|
||||
|
||||
@ -335,7 +348,7 @@ public final class PluginManager
|
||||
} );
|
||||
} catch ( Throwable t )
|
||||
{
|
||||
proxy.getLogger().log( Level.WARNING, "Error enabling plugin " + plugin.getName(), t );
|
||||
proxy.getLogger().log( Level.WARNING, "Error loading plugin " + plugin.getName(), t );
|
||||
}
|
||||
}
|
||||
|
||||
@ -463,4 +476,19 @@ public final class PluginManager
|
||||
{
|
||||
return Collections.unmodifiableCollection( commandMap.entrySet() );
|
||||
}
|
||||
|
||||
boolean isTransitiveDepend(PluginDescription plugin, PluginDescription depend)
|
||||
{
|
||||
Preconditions.checkArgument( plugin != null, "plugin" );
|
||||
Preconditions.checkArgument( depend != null, "depend" );
|
||||
|
||||
if ( dependencyGraph.nodes().contains( plugin.getName() ) )
|
||||
{
|
||||
if ( Graphs.reachableNodes( dependencyGraph, plugin.getName() ).contains( depend.getName() ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,31 @@
|
||||
<version>5.1.49</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- add these back in as they are not exposed by the API -->
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-resolver-provider</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.resolver</groupId>
|
||||
<artifactId>maven-resolver-connector-basic</artifactId>
|
||||
<version>1.6.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.resolver</groupId>
|
||||
<artifactId>maven-resolver-transport-http</artifactId>
|
||||
<version>1.6.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-jdk14</artifactId>
|
||||
<version>1.7.30</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
Loading…
Reference in New Issue
Block a user