Как переупаковать HttpClient 4.3.1 и удалить зависимости от общего журнала?
Я хочу упаковать httpclient lib в Apache, чтобы отправить его с приложением для Android (например, https://code.google.com/p/httpclientandroidlib/ но с HttpClient 4.3.1)
Поэтому я скачал jar httpclient 4.3.1 (включает все его зависимости) вручную и использовал jarjar для его перепаковки:
x@x$: cd libs && for f in *.jar; do java -jar ../jarjar-1.4.jar process ../rules.txt $f out/my-$f; done
с rules.txt:
rule org.apache.http.** my.repackaged.org.apache.http.@1
Затем я использовал ant, чтобы соединить вывод:
<project name="MyProject" default="merge" basedir=".">
<target name="merge">
<zip destfile="my-org-apache-httpclient-4.3.1.jar">
<zipgroupfileset dir="libs/out" includes="*.jar"/>
</zip>
</target>
</project>
Я могу использовать этот файл для разработки и тестирования моего приложения, но если я разверну его на Android, он выдаст исключение s / th, как будто он не может найти my.repackaged.org.apache.logging.log4j.something
ссылается на my.package.org.apache.logging.whatEver
,
Итак, теперь я хочу избавиться от любой зависимости от ведения журнала с помощью использования байт-кода. Это было сделано раньше: http://sixlegs.com/blog/java/dependency-killer.html
Но мне интересно, как я на самом деле это делаю? Есть только зависимости от org.apache.commons.logging.Log:
x$x$: java -jar jarjar-1.4.jar find jar my-org-apache-httpclient-4.3.1.jar commons-logging-1.1.3.jar
my/http/impl/execchain/ServiceUnavailableRetryExec -> org/apache/commons/logging/Log
my/http/impl/execchain/RetryExec -> org/apache/commons/logging/Log
my/http/impl/execchain/RedirectExec -> org/apache/commons/logging/Log
my/http/impl/execchain/ProtocolExec -> org/apache/commons/logging/Log
...
Я думаю, что путь состоит в том, чтобы удалить эти зависимости и заменить его собственной реализацией, как он сделал здесь https://code.google.com/p/httpclientandroidlib/. Поэтому я сделал новый проект Maven только с одним классом с provided
область для регистрации общего достояния, которая реализует интерфейс org.apache.commons.logging.Log и просто удаляет файлы в android.utils.Log
:
MyLog implements org.apache.commons.logging.Log {}
в упаковке my.log
и я упаковал это в my-log-1.0.0.jar. Я поместил этот jar в ту же папку, что и перепакованный httpclient-jars, и использовал ant, как упомянуто выше, чтобы упаковать все вместе в my-org-apache-httpclient-4.3.1.jar.
Подход 1
Я попытался использовать jarjar снова:
java -jar jarjar-1.4.jar process rules2.txt my-org-apache-httpclient-4.3.1.jar my-org-apache-httpclient-4.3.1-without-logging-dep.jar
с rules2.txt:
rule my.repackaged.commons.logging.** my.log.@1
но это не работает. Исключение, которое он не может найти my.repackaged.org.apache.logging.log4j.something
ссылается на my.package.org.apache.logging.whatEver
все еще брошен.
Подход 2
Я также попытался удалить содержимое журнала из окончательного jar и / или перепаковать my.repackaged.org.apache.log4j и войти в его исходные пакеты:
rules2.txt v2:
rule my.repackaged.org.apache.log4j.** org.apache.log4j.@1
rule my.repackaged.org.apache.logging.** org.apache.logging.@1
но это также все еще бросает исключение: my.repackaged.org.apache.logging.log4j.something
ссылается на my.package.org.apache.logging.whatEver
ВОПРОС
Как я могу убить / заменить эти зависимости общего журнала и избавиться от исключения?
1 ответ
Вступление
Если программа зависит от библиотеки, это обычно означает, что она использует методы библиотеки. Поэтому удаление зависимости - непростая задача. Вы фактически хотите убрать код, который - по крайней мере, формально - требуется программой.
Существует три способа удаления зависимостей:
- Адаптируйте исходный код, чтобы он не зависел от библиотеки, и скомпилируйте его с нуля.
- Измените байт-код, чтобы удалить ссылки на библиотеку, от которой зависит проект.
- Управляйте временем выполнения, чтобы не требовать зависимости. Самый простой способ - воссоздать необходимые классы и поместить их в файл JAR.
Ни один из этих способов не очень хорош. Все они могут потребовать много работы. Ни один из них не гарантированно работает без побочных эффектов.
Решение
Я опишу свое решение, представив файлы и шаги, которые я использовал для решения проблемы. Для воспроизведения вам понадобятся следующие файлы (в одном каталоге):
lib / xxx-vvvjar: библиотека jar (httpclient и зависимости, за исключением commons-logging-1.1.3.jar)
jarjar-1.4.jar: используется для переупаковки банок
rules.txt: правила jarjar
rule org.apache.http.** my.http.@1
rule org.apache.commons.logging.** my.logging.@1
build.xml: конфигурация сборки Ant
<project name="MyProject" basedir=".">
<target name="logimpl">
<javac srcdir="java/src" destdir="java/bin" target="1.5" />
<jar jarfile="out/logimpl.jar" basedir="java/bin" />
</target>
<target name="merge">
<zip destfile="httpclient-4.3.1.jar">
<zipgroupfileset dir="out" includes="*.jar"/>
</zip>
</target>
</project>
Java/ SRC /Log.java
package my.logging;
public interface Log {
public boolean isDebugEnabled();
public void debug(Object message);
public void debug(Object message, Throwable t);
public boolean isInfoEnabled();
public void info(Object message);
public void info(Object message, Throwable t);
public boolean isWarnEnabled();
public void warn(Object message);
public void warn(Object message, Throwable t);
public boolean isErrorEnabled();
public void error(Object message);
public void error(Object message, Throwable t);
public boolean isFatalEnabled();
public void fatal(Object message);
public void fatal(Object message, Throwable t);
}
Java/ SRC / LogFactory.java
package my.logging;
public class LogFactory {
private static Log log;
public static Log getLog(Class<?> clazz) {
return getLog(clazz.getName());
}
public static Log getLog(String name) {
if(log == null) {
log = new Log() {
public boolean isWarnEnabled() { return false; }
public boolean isInfoEnabled() { return false; }
public boolean isFatalEnabled() { return false; }
public boolean isErrorEnabled() {return false; }
public boolean isDebugEnabled() { return false; }
public void warn(Object message, Throwable t) {}
public void warn(Object message) {}
public void info(Object message, Throwable t) {}
public void info(Object message) {}
public void fatal(Object message, Throwable t) {}
public void fatal(Object message) {}
public void error(Object message, Throwable t) {}
public void error(Object message) {}
public void debug(Object message, Throwable t) {}
public void debug(Object message) {}
};
}
return log;
}
}
do_everything.sh
#!/bin/sh
# Repackage library
mkdir -p out
for jf in lib/*.jar; do
java -jar jarjar-1.4.jar process rules.txt $jf `echo $jf | sed 's/lib\//out\//'`
done
# Compile logging implementation
mkdir -p java/bin
ant logimpl
# Merge jar files
ant merge
Вот и все. Откройте консоль и выполните
cd my_directory && ./do_everything.sh
Это создаст папку "out", содержащую отдельные файлы JAR и "httpclient-4.3.1.jar", который является окончательным, независимым и рабочим файлом JAR. Итак, что мы только что сделали?
- Переупакованный httpclient (теперь в
my.http
) - Изменена библиотека для использования
my.logging
вместоorg.apache.commons.logging
- Скомпилированные обязательные классы для использования библиотекой (
my.logging.Log
а такжеmy.logging.LogFactory
). - Объединил перепакованные библиотеки и скомпилированные классы в один файл jar, httpclient-4.3.1.jar.
Довольно просто, не правда ли? Просто прочитайте сценарий оболочки построчно, чтобы обнаружить отдельные шаги. Чтобы проверить, все ли зависимости были удалены, вы можете запустить
java -jar jarjar-1.4.jar find class httpclient-4.3.1.jar commons-logging-1.1.3.jar
Я попробовал сгенерированный файл jar с SE7 и Android 4.4, он работал в обоих случаях (см. Примечания ниже).
Версия файла класса
Каждый файл класса имеет основную версию и вспомогательную версию (оба зависят от компилятора). Android SDK требует, чтобы файлы классов имели основную версию менее 0x33 (так что все до 1.7 / JDK 7). Я добавил target="1.5"
приписать муравью javac
Задача, чтобы сгенерированные файлы классов имели основную версию 0x31 и поэтому могли быть включены в ваше приложение для Android.
Альтернатива (манипулирование байт-кодом)
Ты счастливчик. Ведение журнала (почти всегда) является односторонней операцией. Это едва вызывает побочные эффекты, влияющие на основную программу. Это означает, что удаление общего журнала должно быть возможным, так как это не повлияет на функциональность программы.
Я выбрал второй способ, манипулирование байт-кодом, который вы предложили в своем вопросе. Концепция в основном заключается в следующем (A - httpclient, B - регистрация общего доступа):
- Если тип возвращаемого значения метода A является частью B, тип возвращаемого значения будет изменен на
java.lang.Object
, - Если какой-либо аргумент метода A имеет тип, который является частью B, тип аргумента будет изменен на
java.lang.Object
, - Вызовы методов, принадлежащих B, удаляются полностью.
pop
и постоянные инструкции вставляются для восстановления стека VM. - Типы, принадлежащие B, удаляются из дескрипторов методов, вызываемых из A. Это требует обработки целевого класса (класса, содержащего вызываемый метод). Все типы объектов, принадлежащие B, будут заменены на
java.lang.Object
, - Инструкции, которые пытаются получить доступ к полям классов, принадлежащих B, удаляются.
pop
и постоянные инструкции вставляются для восстановления стека VM. - Если метод пытается получить доступ к полю типа, принадлежащего B, сигнатура поля, на которую ссылается инструкция, изменяется на
java.lang.Object
, Это требует, чтобы целевой класс (класс, содержащий доступное поле) был обработан. - Поля типа, содержащегося в B, но принадлежащие классам A, модифицируются так, чтобы их тип
java.lang.Object
,
Как видите, идея этого состоит в том, чтобы заменить все ссылочные классы на java.lang.Object
и удалить все доступы к членам класса, принадлежащим регистрации общего пользования.
Я не знаю, насколько это надежно, и я не проверял библиотеку после применения манипулятора. Но из того, что я увидел (дизассемблированные файлы классов и отсутствие ошибок VM при загрузке файлов классов), я уверен, что код работает.
Я пытался документировать почти все, что делает программа. Он использует API дерева ASM, который обеспечивает довольно простой доступ к структуре файла класса. И - чтобы избежать ненужных негативных отзывов - это "быстрый и грязный" код. Я не очень много тестировал и держу пари, что есть более быстрые способы манипулирования байт-кодом. Но эта программа, кажется, удовлетворяет потребности ОП, и это все, для чего я ее написал.
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
public class DependencyFinder {
public static void main(String[] args) throws IOException {
if(args.length < 2) return;
DependencyFinder df = new DependencyFinder();
df.analyze(new File(args[0]), new File(args[1]), "org.apache.http/.*", "org.apache.commons.logging..*");
}
@SuppressWarnings("unchecked")
public void analyze(File inputFile, File outputFile, String sClassRegex, String dpClassRegex) throws IOException {
JarFile inJar = new JarFile(inputFile);
JarOutputStream outJar = new JarOutputStream(new FileOutputStream(outputFile));
for(Enumeration<JarEntry> entries = inJar.entries(); entries.hasMoreElements();) {
JarEntry inEntry = entries.nextElement();
InputStream inStream = inJar.getInputStream(inEntry);
JarEntry outEntry = new JarEntry(inEntry.getName());
outEntry.setTime(inEntry.getTime());
outJar.putNextEntry(outEntry);
OutputStream outStream = outJar;
// Only process class files, copy all other resources
if(inEntry.getName().endsWith(".class")) {
// Initialize class reader and writer
ClassReader classReader = new ClassReader(inStream);
ClassWriter classWriter = new ClassWriter(0);
String className = classReader.getClassName();
// Check whether to process this class
if(className.matches(sClassRegex)) {
System.out.println("Processing " + className);
// Parse entire class
ClassNode classNode = new ClassNode(Opcodes.ASM4);
classReader.accept(classNode, 0);
// Check super class and interfaces
String superClassName = classNode.superName;
if(superClassName.matches(dpClassRegex)) {
throw new RuntimeException(className + " extends " + superClassName);
}
for(String iface : (List<String>) classNode.interfaces) {
if(iface.matches(dpClassRegex)) {
throw new RuntimeException(className + " implements " + superClassName);
}
}
// Process methods
for(MethodNode method : (List<MethodNode>) classNode.methods) {
Type methodDesc = Type.getMethodType(method.desc);
boolean changed = false;
// Change return type if necessary
Type retType = methodDesc.getReturnType();
if(retType.getClassName().matches(dpClassRegex)) {
retType = Type.getObjectType("java/lang/Object");
changed = true;
}
// Change argument types if necessary
Type[] argTypes = methodDesc.getArgumentTypes();
for(int i = 0; i < argTypes.length; i++) {
if(argTypes[i].getClassName().matches(dpClassRegex)) {
argTypes[i] = Type.getObjectType("java/lang/Object");
changed = true;
}
}
if(changed) {
// Update method descriptor
System.out.print("Changing " + method.name + methodDesc);
methodDesc = Type.getMethodType(retType, argTypes);
method.desc = methodDesc.getDescriptor();
System.out.println(" to " + methodDesc);
}
// Remove method invocations
InsnList insns = method.instructions;
for(int i = 0; i < insns.size(); i++) {
AbstractInsnNode insn = insns.get(i);
// Ignore all other nodes
if(insn instanceof MethodInsnNode) {
MethodInsnNode mnode = (MethodInsnNode) insn;
Type[] cArgTypes = Type.getArgumentTypes(mnode.desc);
Type cRetType = Type.getReturnType(mnode.desc);
if(mnode.owner.matches(dpClassRegex)) {
// The method belongs to one of the classes we want to get rid of
System.out.println("Removing method call " + mnode.owner + "." +
mnode.name + " in " + method.name);
boolean isStatic = (mnode.getOpcode() == Opcodes.INVOKESTATIC);
if(!isStatic) {
// pop instance
insns.insertBefore(insn, new InsnNode(Opcodes.POP));
}
for(int j = 0; j < cArgTypes.length; j++) {
// pop argument on stack
insns.insertBefore(insn, new InsnNode(Opcodes.POP));
}
// Insert a constant value to repair the stack
if(cRetType.getSort() != Type.VOID) {
InsnNode valueInsn = getValueInstruction(cRetType);
insns.insertBefore(insn, valueInsn);
}
// Remove the actual method call
insns.remove(insn);
// Go back one instruction to not skip the next one
i--;
} else {
changed = false;
if(cRetType.getClassName().matches(dpClassRegex)) {
// Change return type
cRetType = Type.getObjectType("java/lang/Object");
changed = true;
}
for(int j = 0; j < cArgTypes.length; j++) {
if(cArgTypes[j].getClassName().matches(dpClassRegex)) {
// Change argument type
cArgTypes[j] = Type.getObjectType("java/lang/Object");
changed = true;
}
}
if(changed) {
// Update method invocation
System.out.println("Patching method call " + mnode.owner + "." +
mnode.name + " in " + method.name);
mnode.desc = Type.getMethodDescriptor(cRetType, cArgTypes);
}
}
} else if(insn instanceof FieldInsnNode) {
// Yeah I lied... we must not ignore all other instructions
FieldInsnNode fnode = (FieldInsnNode) insn;
Type fieldType = Type.getType(fnode.desc);
if(fnode.owner.matches(dpClassRegex)) {
System.out.println("Removing field access to " + fnode.owner + "." +
fnode.name + " in " + method.name);
// Patch code
switch(fnode.getOpcode()) {
case Opcodes.PUTFIELD:
case Opcodes.GETFIELD:
// Pop instance
insns.insertBefore(insn, new InsnNode(Opcodes.POP));
if(fnode.getOpcode() == Opcodes.PUTFIELD) break;
case Opcodes.GETSTATIC:
// Repair stack
insns.insertBefore(insn, getValueInstruction(fieldType));
break;
default:
throw new RuntimeException("Invalid opcode");
}
// Remove instruction
insns.remove(fnode);
i--;
} else {
if(fieldType.getClassName().matches(dpClassRegex)) {
// Change field type
System.out.println("Patching field access to " + fnode.owner +
"." + fnode.name + " in " + method.name);
fieldType = Type.getObjectType("java/lang/Object");
}
// Update field type
fnode.desc = fieldType.getDescriptor();
}
}
}
}
// Process fields
for(FieldNode field : (List<FieldNode>) classNode.fields) {
Type fieldType = Type.getType(field.desc);
if(fieldType.getClassName().matches(dpClassRegex)) {
System.out.print("Changing " + fieldType.getClassName() + " " + field.name);
fieldType = Type.getObjectType("java/lang/Object");
field.desc = fieldType.getDescriptor();
System.out.println(" to " + fieldType.getClassName());
}
}
// Class processed
classNode.accept(classWriter);
} else {
// Nothing changed
classReader.accept(classWriter, 0);
}
// Write class to JAR entry
byte[] bClass = classWriter.toByteArray();
outStream.write(bClass);
} else {
// Copy file
byte[] buffer = new byte[1024 * 64];
int read;
while((read = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, read);
}
}
outJar.closeEntry();
}
outJar.flush();
outJar.close();
inJar.close();
}
InsnNode getValueInstruction(Type type) {
switch(type.getSort()) {
case Type.INT:
case Type.BOOLEAN:
return new InsnNode(Opcodes.ICONST_0);
case Type.LONG:
return new InsnNode(Opcodes.LCONST_0);
case Type.OBJECT:
case Type.ARRAY:
return new InsnNode(Opcodes.ACONST_NULL);
default:
// I am lazy, I did not implement all types
throw new RuntimeException("Type not implemented: " + type);
}
}
}