Add support for including/excluding dependencies when launching a module
- adding includes/excludes properties for modules - includes and their transitive deps are mandatory, excludes can be pattern-based - supports both individual module launch and aggregation Allow exclusions to apply to all artifacts Fixing classloading issues when LaunchedUrlClassloader is used with JARs from the Maven repository Javadoc enhancements
This commit is contained in:
committed by
Ilayaperumal Gopinathan
parent
745a4bd2dd
commit
d9c4f949c0
@@ -19,8 +19,10 @@ package org.springframework.cloud.stream.module.launcher;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -31,6 +33,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||
import org.springframework.cloud.stream.module.resolver.Coordinates;
|
||||
import org.springframework.cloud.stream.module.resolver.ModuleResolver;
|
||||
import org.springframework.cloud.stream.module.utils.ClassloaderUtils;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -60,11 +63,15 @@ public class ModuleLauncher {
|
||||
|
||||
private static final String DEFAULT_EXTENSION = "jar";
|
||||
|
||||
private static final String DEFAULT_CLASSIFIER = "exec";
|
||||
private static final String DEFAULT_MODULE_CLASSIFIER = "exec";
|
||||
|
||||
private static final Pattern COORDINATES_PATTERN =
|
||||
Pattern.compile("([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)");
|
||||
|
||||
private static final String INCLUDE_DEPENDENCIES_ARG = "includes";
|
||||
|
||||
private static final String EXCLUDE_DEPENDENCIES_ARG = "excludes";
|
||||
|
||||
private final ModuleResolver moduleResolver;
|
||||
|
||||
/**
|
||||
@@ -87,20 +94,21 @@ public class ModuleLauncher {
|
||||
* @param aggregate whether the modules should be aggregated at launch
|
||||
* @param parentArgs a list of arguments for the whole aggregate
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void launch(List<ModuleLaunchRequest> moduleLaunchRequests, boolean aggregate, Map<String,String> parentArgs) {
|
||||
List<ModuleLaunchRequest> reversed = new ArrayList<>(moduleLaunchRequests);
|
||||
Collections.reverse(reversed);
|
||||
if (moduleLaunchRequests.size() == 1 || !aggregate) {
|
||||
launchIndividualModules(moduleLaunchRequests);
|
||||
launchIndividualModules(reversed);
|
||||
}
|
||||
else {
|
||||
launchAggregatedModules(moduleLaunchRequests, toArgArray(parentArgs));
|
||||
launchAggregatedModules(moduleLaunchRequests, parentArgs != null ? parentArgs : Collections.EMPTY_MAP);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void launch(List<ModuleLaunchRequest> moduleLaunchRequests, boolean aggregate) {
|
||||
this.launch(moduleLaunchRequests, aggregate, Collections.EMPTY_MAP);
|
||||
this.launch(moduleLaunchRequests, aggregate, Collections.<String,String>emptyMap());
|
||||
}
|
||||
|
||||
public void launch(List<ModuleLaunchRequest> moduleLaunchRequests) {
|
||||
@@ -125,53 +133,106 @@ public class ModuleLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
public void launchAggregatedModules(List<ModuleLaunchRequest> moduleLaunchRequests, final String[] parentArgs) {
|
||||
public void launchAggregatedModules(List<ModuleLaunchRequest> moduleLaunchRequests, Map<String,String> parentArgs) {
|
||||
try {
|
||||
List<String> mainClassNames = new ArrayList<>();
|
||||
List<URL> jarURLs = new ArrayList<>();
|
||||
LinkedHashSet<URL> jarURLs = new LinkedHashSet<>();
|
||||
List<String> seenArchives = new ArrayList<>();
|
||||
final List<String[]> arguments = new ArrayList<>();
|
||||
// aggregate jars from all modules and extract their main Classes
|
||||
for (ModuleLaunchRequest moduleLaunchRequest : moduleLaunchRequests) {
|
||||
Resource resource = resolveModule(moduleLaunchRequest.getModule());
|
||||
JarFileArchive jarFileArchive = new JarFileArchive(resource.getFile());
|
||||
jarURLs.add(jarFileArchive.getUrl());
|
||||
for (Archive archive : jarFileArchive.getNestedArchives(ArchiveMatchingEntryFilter.FILTER)) {
|
||||
// avoid duplication based on unique JAR names
|
||||
// TODO - read the metadata from the JARs, do proper version resolution on merge
|
||||
String urlAsString = archive.getUrl().toString();
|
||||
String jarNameWithExtension = urlAsString.substring(0, urlAsString.lastIndexOf("!/"));
|
||||
String jarNameWithoutExtension = jarNameWithExtension.substring(jarNameWithExtension.lastIndexOf("/") + 1);
|
||||
if (!seenArchives.contains(jarNameWithoutExtension)) {
|
||||
seenArchives.add(jarNameWithoutExtension);
|
||||
jarURLs.add(archive.getUrl());
|
||||
final ClassLoader classLoader;
|
||||
if (!(parentArgs.containsKey(EXCLUDE_DEPENDENCIES_ARG) ||
|
||||
parentArgs.containsKey(INCLUDE_DEPENDENCIES_ARG))) {
|
||||
for (ModuleLaunchRequest moduleLaunchRequest : moduleLaunchRequests) {
|
||||
Resource resource = resolveModule(moduleLaunchRequest.getModule());
|
||||
JarFileArchive jarFileArchive = new JarFileArchive(resource.getFile());
|
||||
jarURLs.add(jarFileArchive.getUrl());
|
||||
for (Archive archive : jarFileArchive.getNestedArchives(ArchiveMatchingEntryFilter.FILTER)) {
|
||||
// avoid duplication based on unique JAR names
|
||||
// TODO - read the metadata from the JARs, do proper version resolution on merge
|
||||
String urlAsString = archive.getUrl().toString();
|
||||
String jarNameWithExtension = urlAsString.substring(0, urlAsString.lastIndexOf("!/"));
|
||||
String jarNameWithoutExtension =
|
||||
jarNameWithExtension.substring(jarNameWithExtension.lastIndexOf("/") + 1);
|
||||
if (!seenArchives.contains(jarNameWithoutExtension)) {
|
||||
seenArchives.add(jarNameWithoutExtension);
|
||||
jarURLs.add(archive.getUrl());
|
||||
}
|
||||
}
|
||||
mainClassNames.add(jarFileArchive.getMainClass());
|
||||
arguments.add(toArgArray(moduleLaunchRequest.getArguments()));
|
||||
}
|
||||
mainClassNames.add(jarFileArchive.getMainClass());
|
||||
arguments.add(toArgArray(moduleLaunchRequest.getArguments()));
|
||||
classLoader = ClassloaderUtils.createModuleClassloader(jarURLs.toArray(new URL[jarURLs.size()]));
|
||||
} else {
|
||||
// First, resolve modules and extract main classes - while slightly less efficient than just
|
||||
// doing the same processing after resolution, this ensures that module artifacts are processed
|
||||
// correctly for extracting their main class names. It is not possible in the general case to
|
||||
// identify, after resolution, whether a resource represents a module artifact which was part of the
|
||||
// original request or not. We will include the first module as root and the next as direct dependencies
|
||||
Coordinates root = null;
|
||||
ArrayList<Coordinates> includeCoordinates = new ArrayList<>();
|
||||
for (ModuleLaunchRequest moduleLaunchRequest : moduleLaunchRequests) {
|
||||
Coordinates moduleCoordinates
|
||||
= toCoordinates(moduleLaunchRequest.getModule(), DEFAULT_MODULE_CLASSIFIER);
|
||||
if (root == null) {
|
||||
root = moduleCoordinates;
|
||||
}
|
||||
else {
|
||||
includeCoordinates.add(toCoordinates(moduleLaunchRequest.getModule(),
|
||||
DEFAULT_MODULE_CLASSIFIER));
|
||||
}
|
||||
Resource moduleResource = resolveModule(moduleLaunchRequest.getModule());
|
||||
JarFileArchive moduleArchive = new JarFileArchive(moduleResource.getFile());
|
||||
mainClassNames.add(moduleArchive.getMainClass());
|
||||
arguments.add(toArgArray(moduleLaunchRequest.getArguments()));
|
||||
}
|
||||
for (String include :
|
||||
StringUtils.commaDelimitedListToStringArray(parentArgs.get(INCLUDE_DEPENDENCIES_ARG))) {
|
||||
includeCoordinates.add(toCoordinates(include, ""));
|
||||
}
|
||||
// Resolve all artifacts - since modules have been specified as direct dependencies, they will take
|
||||
// precedence in the resolution order, ensuring that the already resolved artifacts will be returned as
|
||||
// part of the response.
|
||||
Resource[] libraries = moduleResolver.resolve(root,
|
||||
includeCoordinates.toArray(new Coordinates[includeCoordinates.size()]),
|
||||
StringUtils.commaDelimitedListToStringArray(parentArgs.get(EXCLUDE_DEPENDENCIES_ARG)));
|
||||
for (Resource library : libraries) {
|
||||
jarURLs.add(library.getURL());
|
||||
}
|
||||
classLoader = new URLClassLoader(jarURLs.toArray(new URL[jarURLs.size()]));
|
||||
}
|
||||
final ClassLoader classLoader = ClassloaderUtils
|
||||
.createModuleClassloader(jarURLs.toArray(new URL[jarURLs.size()]));
|
||||
|
||||
final List<Class<?>> mainClasses = new ArrayList<>();
|
||||
for (String mainClass : mainClassNames) {
|
||||
mainClasses.add(ClassUtils.forName(mainClass, classLoader));
|
||||
}
|
||||
Runnable moduleAggregatorRunner = new ModuleAggregatorRunner(classLoader, mainClasses, parentArgs, arguments);
|
||||
|
||||
Runnable moduleAggregatorRunner = new ModuleAggregatorRunner(classLoader, mainClasses,
|
||||
toArgArray(parentArgs), arguments);
|
||||
Thread moduleAggregatorRunnerThread = new Thread(moduleAggregatorRunner);
|
||||
moduleAggregatorRunnerThread.setContextClassLoader(classLoader);
|
||||
moduleAggregatorRunnerThread.setName(MODULE_AGGREGATOR_RUNNER_THREAD_NAME);
|
||||
moduleAggregatorRunnerThread.start();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("failed to start aggregated modules: " + StringUtils.collectionToCommaDelimitedString(moduleLaunchRequests), e);
|
||||
throw new RuntimeException("failed to start aggregated modules: " +
|
||||
StringUtils.collectionToCommaDelimitedString(moduleLaunchRequests), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void launchIndividualModules(List<ModuleLaunchRequest> reversed) {
|
||||
public void launchIndividualModules(List<ModuleLaunchRequest> moduleLaunchRequests) {
|
||||
List<ModuleLaunchRequest> reversed = new ArrayList<>(moduleLaunchRequests);
|
||||
Collections.reverse(reversed);
|
||||
for (ModuleLaunchRequest moduleLaunchRequest : reversed) {
|
||||
String module = moduleLaunchRequest.getModule();
|
||||
moduleLaunchRequest.addArgument("spring.jmx.default-domain", module.replace("/", ".").replace(":", "."));
|
||||
launchModule(module, toArgArray(moduleLaunchRequest.getArguments()));
|
||||
Map<String, String> arguments = moduleLaunchRequest.getArguments();
|
||||
if (arguments.containsKey(INCLUDE_DEPENDENCIES_ARG) || arguments.containsKey(EXCLUDE_DEPENDENCIES_ARG)) {
|
||||
String includes = arguments.get(INCLUDE_DEPENDENCIES_ARG);
|
||||
String excludes = arguments.get(EXCLUDE_DEPENDENCIES_ARG);
|
||||
launchModuleWithDependencies(module, toArgArray(arguments),
|
||||
StringUtils.commaDelimitedListToStringArray(includes),
|
||||
StringUtils.commaDelimitedListToStringArray(excludes));
|
||||
} else {
|
||||
launchModule(module, toArgArray(arguments));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,20 +248,49 @@ public class ModuleLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
private Resource resolveModule(String coordinates) {
|
||||
private void launchModuleWithDependencies(String module, String[] args, String[] includes, String[] excludes) {
|
||||
try {
|
||||
Resource[] libraries = this.moduleResolver.resolve(toCoordinates(module, DEFAULT_MODULE_CLASSIFIER),
|
||||
toCoordinateArray(includes), excludes);
|
||||
List<Archive> archives = new ArrayList<>();
|
||||
for (Resource library : libraries) {
|
||||
archives.add(new JarFileArchive(library.getFile()));
|
||||
}
|
||||
MultiArchiveLauncher jarLauncher = new MultiArchiveLauncher(archives);
|
||||
jarLauncher.launch(args);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("failed to launch module: " + module, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Resource resolveModule(String moduleCoordinates) {
|
||||
Coordinates coordinates = toCoordinates(moduleCoordinates, DEFAULT_MODULE_CLASSIFIER);
|
||||
return this.moduleResolver.resolve(coordinates);
|
||||
}
|
||||
|
||||
private Coordinates toCoordinates(String coordinates, String defaultClassifier) {
|
||||
Matcher matcher = COORDINATES_PATTERN.matcher(coordinates);
|
||||
Assert.isTrue(matcher.matches(), "Bad artifact coordinates " + coordinates
|
||||
+ ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>");
|
||||
String groupId = matcher.group(1);
|
||||
String artifactId = matcher.group(2);
|
||||
String extension = StringUtils.hasLength(matcher.group(4)) ? matcher.group(4) : DEFAULT_EXTENSION;
|
||||
String classifier = StringUtils.hasLength(matcher.group(6)) ? matcher.group(6) : DEFAULT_CLASSIFIER;
|
||||
String classifier = StringUtils.hasLength(matcher.group(6)) ? matcher.group(6) : defaultClassifier;
|
||||
String version = matcher.group(7);
|
||||
return this.moduleResolver.resolve(groupId, artifactId, extension, classifier, version);
|
||||
return new Coordinates(groupId, artifactId, extension, classifier, version);
|
||||
}
|
||||
|
||||
private Coordinates[] toCoordinateArray(String[] coordinateList) {
|
||||
List<Coordinates> result = new ArrayList<>();
|
||||
for (String coordinates : coordinateList) {
|
||||
result.add(toCoordinates(coordinates, ""));
|
||||
}
|
||||
return result.toArray(new Coordinates[result.size()]);
|
||||
}
|
||||
|
||||
private class ModuleAggregatorRunner implements Runnable {
|
||||
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
private final String[] parentArgs;
|
||||
@@ -209,7 +299,8 @@ public class ModuleLauncher {
|
||||
|
||||
private final List<String[]> arguments;
|
||||
|
||||
public ModuleAggregatorRunner(ClassLoader classLoader, List<Class<?>> mainClasses, String[] parentArgs, List<String[]> moduleArguments) {
|
||||
public ModuleAggregatorRunner(ClassLoader classLoader, List<Class<?>> mainClasses, String[] parentArgs,
|
||||
List<String[]> moduleArguments) {
|
||||
this.classLoader = classLoader;
|
||||
this.parentArgs = parentArgs;
|
||||
this.mainClasses = mainClasses;
|
||||
@@ -227,7 +318,8 @@ public class ModuleLauncher {
|
||||
mainClasses.toArray(new Class<?>[mainClasses.size()]),
|
||||
parentArgs, arguments.toArray(new String[][] {}));
|
||||
} catch (Exception e) {
|
||||
log.error("failed to launch aggregated modules :" + StringUtils.collectionToCommaDelimitedString(mainClasses), e);
|
||||
log.error("failed to launch aggregated modules :"
|
||||
+ StringUtils.collectionToCommaDelimitedString(mainClasses), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.cloud.stream.module.launcher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -65,7 +67,9 @@ public class ModuleLauncherRunner implements CommandLineRunner {
|
||||
}
|
||||
this.moduleLauncher.launch(launchRequests,
|
||||
moduleLauncherProperties.isAggregate(),
|
||||
moduleLauncherProperties.isAggregate() ? moduleLauncherProperties.getArgs().get(AGGREGATE_ARGS_KEY) : null);
|
||||
moduleLauncherProperties.isAggregate() ?
|
||||
moduleLauncherProperties.getArgs().get(AGGREGATE_ARGS_KEY) :
|
||||
Collections.<String,String>emptyMap());
|
||||
}
|
||||
|
||||
private List<ModuleLaunchRequest> generateModuleLaunchRequests() {
|
||||
@@ -92,6 +96,4 @@ public class ModuleLauncherRunner implements CommandLineRunner {
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.module.launcher;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.loader.Launcher;
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.cloud.stream.module.utils.ClassloaderUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* A {@link Launcher} for multiple independent JAR archives (which aren't nested in an uberjar). This class
|
||||
* supports module aggregation and direct binding.
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class MultiArchiveLauncher extends Launcher {
|
||||
|
||||
private static Log log = LogFactory.getLog(MultiArchiveLauncher.class);
|
||||
|
||||
private final List<Archive> archives;
|
||||
|
||||
/**
|
||||
* A list of archives, the first of which is expected to be a Spring boot uberJar
|
||||
*
|
||||
* @param archives
|
||||
*/
|
||||
public MultiArchiveLauncher(List<Archive> archives) {
|
||||
Assert.notEmpty(archives, "A list of archives must be provided");
|
||||
this.archives = archives;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMainClass() throws Exception {
|
||||
return archives.get(0).getManifest().getMainAttributes().getValue("Start-Class");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Archive> getClassPathArchives() throws Exception {
|
||||
return archives;
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO: this method is protected in Spring Boot but we need it to be public here
|
||||
public void launch(String[] args) {
|
||||
super.launch(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
|
||||
throws Exception {
|
||||
if (log.isDebugEnabled()) {
|
||||
for (Archive archive : archives) {
|
||||
log.debug("Launching with: " + archive.getUrl().toString());
|
||||
}
|
||||
}
|
||||
if (ClassUtils.isPresent(
|
||||
"org.apache.catalina.webresources.TomcatURLStreamHandlerFactory",
|
||||
classLoader)) {
|
||||
// Ensure the method is invoked on a class that is loaded by the provided
|
||||
// class loader (not the current context class loader):
|
||||
Method method = ReflectionUtils
|
||||
.findMethod(
|
||||
classLoader
|
||||
.loadClass("org.apache.catalina.webresources.TomcatURLStreamHandlerFactory"),
|
||||
"disable");
|
||||
ReflectionUtils.invokeMethod(method, null);
|
||||
}
|
||||
super.launch(args, mainClass, classLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
|
||||
return new URLClassLoader(urls);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.cloud.stream.module.resolver;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -29,22 +30,29 @@ import org.eclipse.aether.RepositorySystem;
|
||||
import org.eclipse.aether.RepositorySystemSession;
|
||||
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.resolution.ArtifactRequest;
|
||||
import org.eclipse.aether.resolution.ArtifactResolutionException;
|
||||
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.transport.file.FileTransporterFactory;
|
||||
import org.eclipse.aether.transport.http.HttpTransporterFactory;
|
||||
import org.eclipse.aether.util.artifact.JavaScopes;
|
||||
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -108,36 +116,13 @@ public class AetherModuleResolver implements ModuleResolver {
|
||||
/**
|
||||
* Resolve an artifact and return its location in the local repository. Aether performs the normal
|
||||
* Maven resolution process ensuring that the latest update is cached to the local repository.
|
||||
* @param groupId the groupId
|
||||
* @param artifactId the artifactId
|
||||
* @param extension the file extension
|
||||
* @param classifier classifier can be null if none
|
||||
* @param version the version
|
||||
* @param coordinates the Maven coordinates of the artifact
|
||||
* @return a {@link FileSystemResource} representing the resolved artifact in the local repository
|
||||
* @throws RuntimeException if the artifact does not exist or the resolution fails
|
||||
*/
|
||||
@Override
|
||||
public Resource resolve(String groupId, String artifactId, String extension, String classifier, String version) {
|
||||
Assert.hasText(groupId, "'groupId' cannot be blank.");
|
||||
Assert.hasText(artifactId, "'artifactId' cannot be blank.");
|
||||
Assert.hasText(extension, "'extension' cannot be blank.");
|
||||
if (classifier == null) {
|
||||
classifier = "";
|
||||
}
|
||||
Assert.hasText(version, "'version' cannot be blank.");
|
||||
|
||||
Artifact artifact = new DefaultArtifact(groupId, artifactId, classifier, extension, version);
|
||||
RepositorySystemSession session = newRepositorySystemSession(repositorySystem,
|
||||
localRepository.getAbsolutePath());
|
||||
ArtifactResult result;
|
||||
try {
|
||||
result = repositorySystem.resolveArtifact(session,
|
||||
new ArtifactRequest(artifact, remoteRepositories, "runtime"));
|
||||
}
|
||||
catch (ArtifactResolutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return new FileSystemResource(result.getArtifact().getFile());
|
||||
public Resource resolve(Coordinates coordinates) {
|
||||
return this.resolve(coordinates, null, null)[0];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -169,4 +154,89 @@ public class AetherModuleResolver implements ModuleResolver {
|
||||
});
|
||||
return locator.getService(RepositorySystem.class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolve a set of artifacts based on their coordinates, including their dependencies, and return the locations of
|
||||
* the transitive set in the local repository. Aether performs the normal Maven resolution process ensuring that the
|
||||
* latest update is cached to the local repository. A number of additional includes and excludes can be specified,
|
||||
* allowing to override the transitive dependencies of the original set. Includes and their transitive dependencies
|
||||
* will always
|
||||
*
|
||||
* @param root the Maven coordinates of the artifacts
|
||||
* @return a {@link FileSystemResource} representing the resolved artifact in the local repository
|
||||
* @throws RuntimeException if the artifact does not exist or the resolution fails
|
||||
*/
|
||||
@Override
|
||||
public Resource[] resolve(Coordinates root, Coordinates[] includes, String[] excludePatterns) {
|
||||
Assert.notNull(root, "Root cannot be null");
|
||||
validateCoordinates(root);
|
||||
if (!ObjectUtils.isEmpty(includes)) {
|
||||
for (Coordinates include : includes) {
|
||||
Assert.notNull(include, "Includes cannot be null");
|
||||
validateCoordinates(include);
|
||||
}
|
||||
}
|
||||
List<Resource> result = new ArrayList<>();
|
||||
Artifact rootArtifact = toArtifact(root);
|
||||
RepositorySystemSession session = newRepositorySystemSession(repositorySystem,
|
||||
localRepository.getAbsolutePath());
|
||||
if (ObjectUtils.isEmpty(includes) && ObjectUtils.isEmpty(excludePatterns)) {
|
||||
ArtifactResult resolvedArtifact;
|
||||
try {
|
||||
resolvedArtifact = repositorySystem.resolveArtifact(session,
|
||||
new ArtifactRequest(rootArtifact, remoteRepositories, JavaScopes.RUNTIME));
|
||||
}
|
||||
catch (ArtifactResolutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
result.add(toResource(resolvedArtifact));
|
||||
}
|
||||
else {
|
||||
try {
|
||||
CollectRequest collectRequest = new CollectRequest();
|
||||
collectRequest.setRepositories(remoteRepositories);
|
||||
collectRequest.setRoot(new Dependency(rootArtifact, JavaScopes.RUNTIME));
|
||||
Artifact[] includeArtifacts = new Artifact[!ObjectUtils.isEmpty(includes) ? includes.length : 0];
|
||||
int i = 0;
|
||||
for (Coordinates include : includes) {
|
||||
Artifact includedArtifact = toArtifact(include);
|
||||
collectRequest.addDependency(new Dependency(includedArtifact, JavaScopes.RUNTIME));
|
||||
includeArtifacts[i++] = includedArtifact;
|
||||
}
|
||||
DependencyResult dependencyResult =
|
||||
repositorySystem.resolveDependencies(session,
|
||||
new DependencyRequest(collectRequest,
|
||||
new InclusionExclusionDependencyFilter(includeArtifacts, excludePatterns)));
|
||||
for (ArtifactResult artifactResult : dependencyResult.getArtifactResults()) {
|
||||
// we are only interested in the jars
|
||||
if ("jar".equalsIgnoreCase(artifactResult.getArtifact().getExtension())) {
|
||||
result.add(toResource(artifactResult));
|
||||
}
|
||||
}
|
||||
} catch (DependencyResolutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return result.toArray(new Resource[result.size()]);
|
||||
}
|
||||
|
||||
private void validateCoordinates(Coordinates coordinates) {
|
||||
Assert.hasText(coordinates.getGroupId(), "'groupId' cannot be blank.");
|
||||
Assert.hasText(coordinates.getArtifactId(), "'artifactId' cannot be blank.");
|
||||
Assert.hasText(coordinates.getExtension(), "'extension' cannot be blank.");
|
||||
Assert.hasText(coordinates.getVersion(), "'version' cannot be blank.");
|
||||
}
|
||||
|
||||
public FileSystemResource toResource(ArtifactResult resolvedArtifact) {
|
||||
return new FileSystemResource(resolvedArtifact.getArtifact().getFile());
|
||||
}
|
||||
|
||||
private Artifact toArtifact(Coordinates root) {
|
||||
return new DefaultArtifact(root.getGroupId(),
|
||||
root.getArtifactId(),
|
||||
root.getClassifier() != null ? root.getClassifier() : "",
|
||||
root.getExtension(),
|
||||
root.getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.module.resolver;
|
||||
|
||||
/**
|
||||
* Encapsulates Maven coordinates.
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class Coordinates {
|
||||
|
||||
private final String groupId;
|
||||
|
||||
private final String artifactId;
|
||||
|
||||
private final String extension;
|
||||
|
||||
private String classifier;
|
||||
|
||||
private final String version;
|
||||
|
||||
/**
|
||||
* @param groupId the groupId
|
||||
* @param artifactId the artifactId
|
||||
* @param extension the file extension
|
||||
* @param classifier classifier
|
||||
* @param version the version
|
||||
*/
|
||||
public Coordinates(String groupId, String artifactId, String extension, String classifier, String version) {
|
||||
this.groupId = groupId;
|
||||
this.artifactId = artifactId;
|
||||
this.extension = extension;
|
||||
this.classifier = classifier;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public String getArtifactId() {
|
||||
return artifactId;
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return extension;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.module.resolver;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.aether.artifact.Artifact;
|
||||
import org.eclipse.aether.graph.DependencyFilter;
|
||||
import org.eclipse.aether.graph.DependencyNode;
|
||||
import org.eclipse.aether.util.filter.PatternExclusionsDependencyFilter;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A {@link DependencyFilter} that uses a list of explicit includes and pattern-based excludes. Any items that match
|
||||
* the exclusion pattern will be rejected, unless they are accepted explicitly
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class InclusionExclusionDependencyFilter implements DependencyFilter {
|
||||
|
||||
private final PatternExclusionsDependencyFilter patternExclusionsDependencyFilter;
|
||||
|
||||
private final Artifact[] includes;
|
||||
|
||||
public InclusionExclusionDependencyFilter(Artifact[] includes, String... excludes) {
|
||||
this.patternExclusionsDependencyFilter = new PatternExclusionsDependencyFilter(excludes);
|
||||
this.includes = includes != null ? includes : new Artifact[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(DependencyNode node, List<DependencyNode> parents) {
|
||||
// nodes included explicitly are always accepted
|
||||
return isIncludedDirectly(node) || patternExclusionsDependencyFilter.accept(node, parents);
|
||||
}
|
||||
|
||||
private boolean isIncludedDirectly(DependencyNode node) {
|
||||
if (node.getArtifact() != null) {
|
||||
for (Artifact include : includes) {
|
||||
Artifact nodeArtifact = node.getArtifact();
|
||||
// we check if this was a specifically included artifact by checking its group, artifactId, extension and
|
||||
// classifier. The version is left out in the case when resolution produces a different artifact version
|
||||
// (there cannot be two artifacts with the same group, artifactId, extension and classifier but different
|
||||
// version in the resolved group)
|
||||
if (ObjectUtils.nullSafeEquals(include.getGroupId(), nodeArtifact.getGroupId())
|
||||
&& ObjectUtils.nullSafeEquals(include.getArtifactId(), nodeArtifact.getArtifactId())
|
||||
&& ObjectUtils.nullSafeEquals(include.getClassifier(), nodeArtifact.getClassifier())
|
||||
&& ObjectUtils.nullSafeEquals(include.getExtension(), nodeArtifact.getExtension())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -23,19 +23,30 @@ import org.springframework.core.io.Resource;
|
||||
* uber-jar based on its Maven coordinates.
|
||||
*
|
||||
* @author David Turanski
|
||||
* @author Marius Bogoevici
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public interface ModuleResolver {
|
||||
|
||||
/**
|
||||
* Retrieve a resource given its coordinates.
|
||||
*
|
||||
* @param groupId the groupId
|
||||
* @param artifactId the artifactId
|
||||
* @param extension the file extension
|
||||
* @param classifier classifier
|
||||
* @param version the version
|
||||
* @param coordinates the coordinates of a resource
|
||||
* @return the resource
|
||||
*/
|
||||
Resource resolve(String groupId, String artifactId, String extension, String classifier, String version);
|
||||
Resource resolve(Coordinates coordinates);
|
||||
|
||||
/**
|
||||
* Retrieve a set of resources given their coordinates, along with additional dependencies.
|
||||
* Exclusion rules patterns (conforming to {@link org.eclipse.aether.util.filter.PatternExclusionsDependencyFilter}
|
||||
* can be provided as well.
|
||||
*
|
||||
* @param root the coordinates of the main resource
|
||||
* @param includes a list of coordinates to include along the main resource
|
||||
* @param excludePatterns a list of exclusion patterns
|
||||
* @see org.eclipse.aether.util.filter.PatternExclusionsDependencyFilter
|
||||
* @return the main resource and the additional dependencies
|
||||
*/
|
||||
Resource[] resolve(Coordinates root, Coordinates[] includes, String[] excludePatterns);
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
import org.springframework.boot.loader.LaunchedURLClassLoader;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
|
||||
@@ -61,7 +61,7 @@ public class AetherModuleResolverTests {
|
||||
ClassPathResource cpr = new ClassPathResource("local-repo");
|
||||
File localRepository = cpr.getFile();
|
||||
AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, null);
|
||||
Resource resource = defaultModuleResolver.resolve("foo.bar", "foo-bar", "jar", "", "1.0.0");
|
||||
Resource resource = defaultModuleResolver.resolve(new Coordinates("foo.bar", "foo-bar", "jar", "", "1.0.0"));
|
||||
assertTrue(resource.exists());
|
||||
assertEquals(resource.getFile().getName(), "foo-bar-1.0.0.jar");
|
||||
}
|
||||
@@ -71,7 +71,7 @@ public class AetherModuleResolverTests {
|
||||
ClassPathResource cpr = new ClassPathResource("local-repo");
|
||||
File localRepository = cpr.getFile();
|
||||
AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, null);
|
||||
defaultModuleResolver.resolve("niente", "nada", "jar", "", "zilch");
|
||||
defaultModuleResolver.resolve(new Coordinates("niente", "nada", "jar", "", "zilch"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,8 +82,8 @@ public class AetherModuleResolverTests {
|
||||
Map<String, String> remoteRepos = new HashMap<>();
|
||||
remoteRepos.put("modules", "http://repo.spring.io/libs-snapshot");
|
||||
AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, remoteRepos);
|
||||
Resource resource = defaultModuleResolver.resolve("org.springframework.cloud.stream.module", "time-source",
|
||||
"jar", "exec", "1.0.0.BUILD-SNAPSHOT");
|
||||
Resource resource = defaultModuleResolver.resolve(
|
||||
new Coordinates("org.springframework.cloud.stream.module", "time-source", "jar", "exec", "1.0.0.BUILD-SNAPSHOT"));
|
||||
assertTrue(resource.exists());
|
||||
assertEquals(resource.getFile().getName(), "time-source-1.0.0.BUILD-SNAPSHOT-exec.jar");
|
||||
}
|
||||
@@ -102,7 +102,7 @@ public class AetherModuleResolverTests {
|
||||
.withStatus(200)
|
||||
.withBodyFile(stubFileName)));
|
||||
AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, remoteRepos);
|
||||
Resource resource = defaultModuleResolver.resolve("org.bar", "foo", "jar", "", "1.0.0");
|
||||
Resource resource = defaultModuleResolver.resolve(new Coordinates("org.bar", "foo", "jar", "", "1.0.0"));
|
||||
assertTrue(resource.exists());
|
||||
assertEquals(resource.getFile().getName(), "foo-1.0.0.jar");
|
||||
}
|
||||
@@ -123,7 +123,7 @@ public class AetherModuleResolverTests {
|
||||
.withBodyFile(stubFileName)));
|
||||
AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, remoteRepos);
|
||||
defaultModuleResolver.setOffline(true);
|
||||
defaultModuleResolver.resolve("org.bar", "foo", "jar", "", "1.0.0");
|
||||
defaultModuleResolver.resolve(new Coordinates("org.bar", "foo", "jar", "", "1.0.0"));
|
||||
} catch (RuntimeException e) {
|
||||
// remote resolution fails because the resolver is operating offline
|
||||
assertThat(e.getCause(), instanceOf(ArtifactResolutionException.class));
|
||||
|
||||
@@ -20,10 +20,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.annotation.EnableBinding;
|
||||
import org.springframework.cloud.stream.binding.BindableProxyFactory;
|
||||
import org.springframework.cloud.stream.messaging.Processor;
|
||||
import org.springframework.cloud.stream.messaging.Sink;
|
||||
import org.springframework.cloud.stream.messaging.Source;
|
||||
import org.springframework.cloud.stream.binding.BindableProxyFactory;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
|
||||
Reference in New Issue
Block a user