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>
|
<version>${netty.version}</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>org.yaml</groupId>
|
<groupId>org.yaml</groupId>
|
||||||
<artifactId>snakeyaml</artifactId>
|
<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;
|
package net.md_5.bungee.api.plugin;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
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.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
|
import java.security.CodeSigner;
|
||||||
|
import java.security.CodeSource;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
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;
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
|
||||||
|
@ToString(of = "desc")
|
||||||
final class PluginClassloader extends URLClassLoader
|
final class PluginClassloader extends URLClassLoader
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -14,6 +25,10 @@ final class PluginClassloader extends URLClassLoader
|
|||||||
//
|
//
|
||||||
private final ProxyServer proxy;
|
private final ProxyServer proxy;
|
||||||
private final PluginDescription desc;
|
private final PluginDescription desc;
|
||||||
|
private final JarFile jar;
|
||||||
|
private final Manifest manifest;
|
||||||
|
private final URL url;
|
||||||
|
private final ClassLoader libraryLoader;
|
||||||
//
|
//
|
||||||
private Plugin plugin;
|
private Plugin plugin;
|
||||||
|
|
||||||
@ -22,11 +37,18 @@ final class PluginClassloader extends URLClassLoader
|
|||||||
ClassLoader.registerAsParallelCapable();
|
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.proxy = proxy;
|
||||||
this.desc = desc;
|
this.desc = desc;
|
||||||
|
this.jar = new JarFile( file );
|
||||||
|
this.manifest = jar.getManifest();
|
||||||
|
this.url = file.toURI().toURL();
|
||||||
|
this.libraryLoader = libraryLoader;
|
||||||
|
|
||||||
allLoaders.add( this );
|
allLoaders.add( this );
|
||||||
}
|
}
|
||||||
@ -34,10 +56,10 @@ final class PluginClassloader extends URLClassLoader
|
|||||||
@Override
|
@Override
|
||||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
|
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
|
try
|
||||||
{
|
{
|
||||||
@ -45,6 +67,17 @@ final class PluginClassloader extends URLClassLoader
|
|||||||
} catch ( ClassNotFoundException ex )
|
} catch ( ClassNotFoundException ex )
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( checkLibraries && libraryLoader != null )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return libraryLoader.loadClass( name );
|
||||||
|
} catch ( ClassNotFoundException ex )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( checkOther )
|
if ( checkOther )
|
||||||
{
|
{
|
||||||
for ( PluginClassloader loader : allLoaders )
|
for ( PluginClassloader loader : allLoaders )
|
||||||
@ -53,13 +86,66 @@ final class PluginClassloader extends URLClassLoader
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return loader.loadClass0( name, resolve, false );
|
return loader.loadClass0( name, resolve, false, proxy.getPluginManager().isTransitiveDepend( desc, loader.desc ) );
|
||||||
} catch ( ClassNotFoundException ex )
|
} 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 );
|
throw new ClassNotFoundException( name );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ package net.md_5.bungee.api.plugin;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -48,4 +50,8 @@ public class PluginDescription
|
|||||||
* Optional description.
|
* Optional description.
|
||||||
*/
|
*/
|
||||||
private String description = null;
|
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.ArrayListMultimap;
|
||||||
import com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.common.eventbus.Subscribe;
|
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.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -49,6 +51,8 @@ public final class PluginManager
|
|||||||
private final Yaml yaml;
|
private final Yaml yaml;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
private final Map<String, Plugin> plugins = new LinkedHashMap<>();
|
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 final Map<String, Command> commandMap = new HashMap<>();
|
||||||
private Map<String, PluginDescription> toLoad = new HashMap<>();
|
private Map<String, PluginDescription> toLoad = new HashMap<>();
|
||||||
private final Multimap<Plugin, Command> commandsByPlugin = ArrayListMultimap.create();
|
private final Multimap<Plugin, Command> commandsByPlugin = ArrayListMultimap.create();
|
||||||
@ -67,6 +71,17 @@ public final class PluginManager
|
|||||||
yaml = new Yaml( yamlConstructor );
|
yaml = new Yaml( yamlConstructor );
|
||||||
|
|
||||||
eventBus = new EventBus( proxy.getLogger() );
|
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;
|
status = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencyGraph.putEdge( plugin.getName(), dependName );
|
||||||
if ( !status )
|
if ( !status )
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@ -320,10 +336,7 @@ public final class PluginManager
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
URLClassLoader loader = new PluginClassloader( proxy, plugin, new URL[]
|
URLClassLoader loader = new PluginClassloader( proxy, plugin, plugin.getFile(), ( libraryLoader != null ) ? libraryLoader.createLoader( plugin ) : null );
|
||||||
{
|
|
||||||
plugin.getFile().toURI().toURL()
|
|
||||||
} );
|
|
||||||
Class<?> main = loader.loadClass( plugin.getMain() );
|
Class<?> main = loader.loadClass( plugin.getMain() );
|
||||||
Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance();
|
Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance();
|
||||||
|
|
||||||
@ -335,7 +348,7 @@ public final class PluginManager
|
|||||||
} );
|
} );
|
||||||
} catch ( Throwable t )
|
} 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() );
|
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>
|
<version>5.1.49</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
Loading…
Reference in New Issue
Block a user