Skip to content

Commit

Permalink
[pinpoint-apm#8890] Merge all lib jars in custom filesystem
Browse files Browse the repository at this point in the history
  • Loading branch information
youngjin.kim2 committed Jun 16, 2022
1 parent 1dd05c8 commit 3a3afa9
Show file tree
Hide file tree
Showing 6 changed files with 638 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,18 @@

package com.navercorp.pinpoint.bootstrap.java9.module;

import com.navercorp.pinpoint.bootstrap.module.Providers;
import com.navercorp.pinpoint.bootstrap.java9.module.merger.JarMerger;

import java.io.Closeable;
import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarFile;

/**
* @author Woonduk Kang(emeroad)
Expand All @@ -54,110 +50,29 @@ Module defineModule(String moduleName, ClassLoader classLoader, URL[] urls) {
logger.info("defineModule classLoader: " + classLoader);
logger.info("defineModule classLoader-unnamedModule: " + unnamedModule);


List<PackageInfo> packageInfos = parsePackageInfo(urls);
Set<String> packages = mergePackageInfo(packageInfos);
logger.info("packages:" + packages);
Map<String, Set<String>> serviceInfoMap = mergeServiceInfo(packageInfos);
logger.info("providers:" + serviceInfoMap);

ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(moduleName);
builder.packages(packages);
for (Map.Entry<String, Set<String>> entry : serviceInfoMap.entrySet()) {
builder.provides(entry.getKey(), new ArrayList<>(entry.getValue()));
}

ModuleDescriptor moduleDescriptor = builder.build();
URI url = getInformationURI(urls);

Module module = InternalModules.defineModule(classLoader, moduleDescriptor , url);
logger.info("defineModule module:" + module);
return module;
}

private Map<String, Set<String>> mergeServiceInfo(List<PackageInfo> packageInfos) {
Map<String, Set<String>> providesMap = new HashMap<>();
for (PackageInfo packageInfo : packageInfos) {
List<Providers> serviceLoader = packageInfo.getProviders();
for (Providers provides : serviceLoader) {
Set<String> providerSet = providesMap.computeIfAbsent(provides.getService(), s -> new HashSet<>());
providerSet.addAll(provides.getProviders());
}
}
return providesMap;
}

private Set<String> mergePackageInfo(List<PackageInfo> packageInfos) {
Set<String> packageSet = new HashSet<>();
for (PackageInfo packageInfo : packageInfos) {
packageSet.addAll(packageInfo.getPackage());
}
return packageSet;
}

private JarFile newJarFile(URL jarFile) {
try {
if (!jarFile.getProtocol().equals("file")) {
throw new IllegalStateException("invalid file " + jarFile);
}
return new JarFile(jarFile.getFile());
} catch (IOException e) {
throw new ModuleException(jarFile.getFile() + " create fail " + e.getMessage(), e);
}
}

private URI getInformationURI(URL[] urls) {
if (isEmpty(urls)) {
return null;
}
final URL url = urls[0];
try {
return url.toURI();
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}

private boolean isEmpty(URL[] urls) {
return urls == null || urls.length == 0;
}

private List<PackageInfo> parsePackageInfo(URL[] urls) {

final List<PackageInfo> packageInfoList = new ArrayList<>();
for (URL url : urls) {
if (!isJar(url)) {
continue;
}
JarFile jarFile = null;
final List<URI> uris = new ArrayList<>(urls.length);
for (final URL url: urls) {
try {
jarFile = newJarFile(url);
PackageAnalyzer packageAnalyzer = new JarFileAnalyzer(jarFile);
PackageInfo packageInfo = packageAnalyzer.analyze();
packageInfoList.add(packageInfo);
} finally {
close(jarFile);
uris.add(url.toURI());
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid lib url", e);
}
}
return packageInfoList;
}

private boolean isJar(URL url){
// filter *.xml
if (url.getPath().endsWith(".jar")) {
return true;
}
return false;
}
final ModuleLayer bootLayer = ModuleLayer.boot();

private void close(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (IOException ignored) {
// skip
final ModuleFinder finder = JarMerger.build(moduleName, uris);
final Configuration config = bootLayer.configuration().resolve(finder, ModuleFinder.of(), Set.of(moduleName));
final Module module = bootLayer.defineModules(config, name -> classLoader)
.modules()
.iterator()
.next();

logger.info("defineModule module:" + module);
return module;
} catch (IOException e) {
throw new RuntimeException("Failed to define module", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2022 NAVER Corp.
*
* 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 com.navercorp.pinpoint.bootstrap.java9.module.merger;

import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;

/**
* @author youngjin.kim2
*/
public class InMemorySeekableByteChannel implements SeekableByteChannel {

private final byte[] bytes;
private long position = 0;

public InMemorySeekableByteChannel(byte[] bytes, long position) {
this(bytes);
this.position = Math.min(position, bytes.length);
}

public InMemorySeekableByteChannel(byte[] bytes) {
this.bytes = bytes;
}

@Override
public int read(ByteBuffer dst) {
if (position >= bytes.length) {
return -1;
}

long remain = size() - position;
int sz = (int) Math.min(remain, dst.capacity());

dst.put(bytes, (int) position, sz);
position += sz;
return sz;
}

@Override
public int write(ByteBuffer src) {
bytes[(int) position++] = src.get();
return 1;
}

@Override
public long position() {
return position;
}

@Override
public SeekableByteChannel position(long newPosition) {
return new InMemorySeekableByteChannel(bytes, newPosition);
}

@Override
public long size() {
return bytes.length;
}

@Override
public SeekableByteChannel truncate(long size) {
throw new RuntimeException("Illegal truncate attempt");
}

@Override
public boolean isOpen() {
return true;
}

@Override
public void close() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2022 NAVER Corp.
*
* 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 com.navercorp.pinpoint.bootstrap.java9.module.merger;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.module.ModuleFinder;
import java.net.URI;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
* @author youngjin.kim2
*/
public class JarMerger {

private final Set<String> entries = new HashSet<>();
private final Map<String, String> providesMap = new HashMap<>();

private final JarOutputStream jarStream;
private final ByteArrayOutputStream byteStream;
private final String moduleName;

private JarMerger(String moduleName, List<URI> uris) throws IOException {
this.moduleName = moduleName;
this.byteStream = new ByteArrayOutputStream();
this.jarStream = new JarOutputStream(byteStream);

entries.add("META-INF/MANIFEST.MF");
entries.add("module-info.class");

for (final URI uri: uris) {
add(uri);
}

addManifest();
addProvides();
this.jarStream.close();
}

private void add(URI uri) throws IOException {
if (!uri.toString().endsWith(".jar")) {
return;
}

final File file = new File(uri);
try (final JarFile jarFile = new JarFile(file)) {
final Enumeration<JarEntry> newEntries = jarFile.entries();
while (newEntries.hasMoreElements()) {
final JarEntry newEntry = newEntries.nextElement();
final String name = newEntry.getName();

if (name.startsWith("META-INF/services/")) {
final String provides = new String(jarFile.getInputStream(newEntry).readAllBytes());
provides(name, provides);
}

if (entries.contains(name)) {
continue;
}
entries.add(newEntry.getName());

addEntry(name, jarFile.getInputStream(newEntry).readAllBytes());
}
}
}

private void provides(String name, String provides) {
providesMap.put(name, providesMap.getOrDefault(name, "") + provides);
}

public static ModuleFinder build(String moduleName, List<URI> uris) throws IOException {
return (new JarMerger(moduleName, uris)).getModuleFinder();
}

private ModuleFinder getModuleFinder() {
return ModuleFinder.of(SingleFileSystem.byteArrayToPath("combined.jar", byteStream.toByteArray()));
}

private void addManifest() throws IOException {
addEntry("META-INF/MANIFEST.MF", getManifestContent().getBytes());
}

private void addProvides() throws IOException {
for (Map.Entry<String, String> e: providesMap.entrySet()) {
addEntry("META-INF/services/" + e.getKey(), e.getValue().getBytes());
}
}

private void addEntry(String name, byte[] bytes) throws IOException {
jarStream.putNextEntry(new JarEntry(name));
jarStream.write(bytes);
jarStream.closeEntry();
}

private String getManifestContent() {
return "Manifest-Version: 1.0\r\nAutomatic-Module-Name: " + moduleName + "\r\n\r\n";
}

}
Loading

0 comments on commit 3a3afa9

Please sign in to comment.