Skip to content

Commit

Permalink
fix apache#4820, support class cache for ByteBuddy, solve the problem…
Browse files Browse the repository at this point in the history
… of other javaagent retransform classes enhanced by SkyWalking.
  • Loading branch information
kylixs committed Jun 4, 2020
1 parent c2e2d41 commit 16da87a
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.skywalking.apm.agent.core.logging.core.LogLevel;
import org.apache.skywalking.apm.agent.core.logging.core.LogOutput;
import org.apache.skywalking.apm.agent.core.logging.core.WriterFactory;
import org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ClassCacheMode;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -72,6 +73,19 @@ public static class Agent {
*/
public static boolean IS_OPEN_DEBUGGING_CLASS = false;

/**
* If true, SkyWalking agent will cache all instrumented classes to memory or disk files (decided by class cache mode),
* allow other javaagent to enhance those classes that enhanced by SkyWalking agent.
*/
public static boolean IS_CACHE_ENHANCED_CLASS = false;

/**
* The instrumented classes cache mode: MEMORY or FILE
* MEMORY: cache class bytes to memory, if instrumented classes is too many or too large, it may take up more memory
* FILE: cache class bytes to user temp folder starts with 'class-cache', automatically clean up cached class files when the application exits
*/
public static ClassCacheMode CLASS_CACHE_MODE = ClassCacheMode.MEMORY;

/**
* The identifier of the instance
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.skywalking.apm.agent.core.plugin.bytebuddy;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
import net.bytebuddy.utility.RandomString;
import org.apache.skywalking.apm.agent.core.logging.api.ILog;
import org.apache.skywalking.apm.agent.core.logging.api.LogManager;
import org.apache.skywalking.apm.agent.core.util.IOUtils;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.IllegalClassFormatException;
import java.nio.file.Files;
import java.security.ProtectionDomain;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Wrapper classFileTransformer of ByteBuddy, save the enhanced bytecode to memory cache or file cache,
* and automatically load the previously generated bytecode during the second retransform,
* to solve the problem that ByteBuddy generates auxiliary classes with different random names every time.
* Allow other javaagent to enhance those classes that enhanced by SkyWalking agent.
*/
public class CacheableTransformerDecorator implements AgentBuilder.TransformerDecorator {

private static final ILog logger = LogManager.getLogger(CacheableTransformerDecorator.class);

private String cacheDirBase;
private final ClassCacheMode cacheMode;
private ClassCacheResolver cacheResolver;

public CacheableTransformerDecorator(ClassCacheMode cacheMode) throws IOException {
this.cacheMode = cacheMode;
initClassCache();
}

public CacheableTransformerDecorator(ClassCacheMode cacheMode, String cacheDirBase) throws IOException {
this.cacheDirBase = cacheDirBase;
this.cacheMode = cacheMode;
initClassCache();
}

private void initClassCache() throws IOException {
if (this.cacheMode.equals(ClassCacheMode.FILE)) {
File cacheDir;
if (this.cacheDirBase == null) {
cacheDir = Files.createTempDirectory("class-cache").toFile();
} else {
cacheDir = new File(this.cacheDirBase + "/class-cache-" + RandomString.make());
}
cacheResolver = new FileCacheResolver(cacheDir);
} else {
cacheResolver = new MemoryCacheResolver();
}
}

@Override
public ResettableClassFileTransformer decorate(ResettableClassFileTransformer classFileTransformer) {
return new ResettableClassFileTransformer.WithDelegation(classFileTransformer) {

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// load from cache
byte[] classCache = cacheResolver.getClassCache(className);
if (classCache != null) {
return classCache;
}

//transform class
classfileBuffer = classFileTransformer.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);

// save to cache
if (classfileBuffer != null) {
cacheResolver.putClassCache(className, classfileBuffer);
}

return classfileBuffer;
}
};
}

interface ClassCacheResolver {

byte[] getClassCache(String className);

void putClassCache(String className, byte[] classfileBuffer);
}

static class MemoryCacheResolver implements ClassCacheResolver {

private Map<String, byte[]> classCacheMap = new ConcurrentHashMap<String, byte[]>();

@Override
public byte[] getClassCache(String className) {
return classCacheMap.get(className);
}

@Override
public void putClassCache(String className, byte[] classfileBuffer) {
classCacheMap.put(className, classfileBuffer);
}
}

static class FileCacheResolver implements ClassCacheResolver {

private final File cacheDir;

FileCacheResolver(File cacheDir) {
this.cacheDir = cacheDir;
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
cacheDir.deleteOnExit();
}

@Override
public byte[] getClassCache(String className) {
// load from cache
File cacheFile = getCacheFile(className);
if (cacheFile.exists()) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(cacheFile);
return IOUtils.toByteArray(fileInputStream);
} catch (IOException e) {
logger.error("load class bytes from cache file failure", e);
} finally {
IOUtils.closeQuietly(fileInputStream);
}
}
return null;
}

@Override
public void putClassCache(String className, byte[] classfileBuffer) {
File cacheFile = getCacheFile(className);
FileOutputStream output = null;
try {
output = new FileOutputStream(cacheFile);
IOUtils.copy(new ByteArrayInputStream(classfileBuffer), output);
} catch (IOException e) {
logger.error("save class bytes to cache file failure", e);
} finally {
IOUtils.closeQuietly(output);
}
}

private File getCacheFile(String className) {
return new File(cacheDir, className.replace('/', '.') + ".class");
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.skywalking.apm.agent.core.plugin.bytebuddy;

/**
* ByteBuddy class cache mode
*/
public enum ClassCacheMode {
FILE, MEMORY
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.skywalking.apm.agent.core.util;

/**
* Copied from {@link org.apache.commons.io.IOUtils}
*/

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class IOUtils {

private static final int EOF = -1;

/**
* The default buffer size ({@value}) to use for
* {@link #copyLarge(InputStream, OutputStream)}
*/
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

/**
* Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
*
* @param input the <code>InputStream</code> to read from
* @return the requested byte array
* @throws NullPointerException if the input is null
* @throws IOException if an I/O error occurs
*/
public static byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
copy(input, output);
return output.toByteArray();
}


/**
* Copy bytes from an <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* Large streams (over 2GB) will return a bytes copied value of
* <code>-1</code> after the copy has completed since the correct
* number of bytes cannot be returned as an int. For large streams
* use the <code>copyLarge(InputStream, OutputStream)</code> method.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @return the number of bytes copied, or -1 if &gt; Integer.MAX_VALUE
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.1
*/
public static int copy(InputStream input, OutputStream output) throws IOException {
long count = copyLarge(input, output);
if (count > Integer.MAX_VALUE) {
return -1;
}
return (int) count;
}

/**
* Copy bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.3
*/
public static long copyLarge(InputStream input, OutputStream output)
throws IOException {
return copyLarge(input, output, new byte[DEFAULT_BUFFER_SIZE]);
}

/**
* Copy bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param buffer the buffer to use for the copy
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(InputStream input, OutputStream output, byte[] buffer)
throws IOException {
long count = 0;
int n = 0;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}

/**
* close streams
* @param closeable the closeable handler
*/
public static void closeQuietly(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException ex) {
//ignore ex
}
}

}
Loading

0 comments on commit 16da87a

Please sign in to comment.