Maven y proguard
Vamos a ver cómo podemos ofuscar y optimizar nuestro código compilado usando Proguard y Maven. Para ello, crearemos un proyecto librería y un proyecto que un main que use dicha librería. Ofuscaremos todo de la librería excepto la parte pública que usará el main y ofuscaremos del main todo excepto el nombre de la clase que tiene el main y el método main, que es necesario para poder arrancar la aplicación.
Tienes todo el código de ejemplo de proguard con maven
Organizamos la librería[editar]
Para facilitar la tarea y porque es una buena costumbre, la librería que hagamos en java debe tener todas las clases que sean susceptibles de ser usadas externamente en un paquete separado. En nuestro ejemplo, las ponemos en
package com.chuidiang.examples.proguard_library.shared;
El resto, en un paquete distinto. En el ejemplo lo hemos puesto aparte, no son subpaquetes de este. Sería
package com.chuidiang.examples.proguard_library.internal;
No necesariamente tiene que ser así, podemos poner lo que queramos donde queramos, pero para la configuración de proguard que pondremos un poco más abajo, es más fácil si la parte pública está en un paquete propio. También es más mantenible esta configuración en el futuro si seguimos tocando la librería añadiendo más clases públicas al exterior o internas.
Estos son los paquetes de la librería
Si miras el código, hemos puesto en la parte pública una interfaz con métodos, una PublicClass con todo tipo (publico, protegido, privado) de métodos y propiedades y un enumerado. El objetivo es únicamente ver lo que nos hará proguard con ello.
En la parte privada (internal) hemos puesto una clase que implementa la interfaz y aunque su código es muy tonto, lo hemos complicado llamando a un método privado.
Plugin de maven para proguard[editar]
En el pom.xml de la librería, ponemos el plugin de maven con su configuración correspondiente
<build>
<plugins>
<!-- Plugin Proguard, para ofuscado de codigo -->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>proguard</goal></goals>
</execution>
</executions>
<configuration>
<obfuscate>true</obfuscate>
<injar>${project.build.finalName}.jar</injar>
<outjar>${project.build.finalName}-small.jar</outjar>
<outputDirectory>${project.build.directory}</outputDirectory>
<proguardInclude>${project.basedir}/proguard.conf</proguardInclude>
<libs>
<!-- Para java 8 -->
<!-- <lib>${java.home}/lib/rt.jar</lib> -->
<!-- <lib>${java.home}/lib/jsse.jar</lib> -->
<!-- Para java 9 o superior -->
<lib>${java.home}/jmods</lib>
</libs>
</configuration>
</plugin>
...
</plugins>
</build>
Básicamente:
- groupId, artifacId y version del plugin
- Que durante la fase de package (cuando se genera el jar de nuestra librería) que se ejecute "proguard"
- La configuración de proguard. Esta requiere verla con algo más de detalle
- obfuscate true para que efectivamente, ofusque.
- injar para decirle qué jar tiene que ofuscar. ${project.build.finalName} es una propiedad de maven que nos da el nombre del jar de la librería sin la extensión.
- outjar para decirle dónde dejar el jar ofuscado. Podemos "machacar" el original, pero suele ser buena constumbre ponerlo en un fichero separado, habitualmente con la coletilla "small". ¿Por qué mantener los dos ficheros jar, el ofuscado y el no ofuscado?. Bien, si luego hay un bug y solo tenemos el ofuscado, seguirlo con el debugger puede ser un infierno.
- outputDirectory en qué directorio dejar el jar ofuscado. ${project.build.directory} es una propiedad de maven que nos da el target dir.
- proguardInclude indica un fichero con más configuración para proguard. En este caso el fichero está en ${project.basedir} que es el raíz de nuestro proyecto (donde tenemos el pom.xml) y el nombre del fichero es proguard.conf
- libs. Proguard, a la hora de ofuscar, necesita conocer las clases padre de nuestras clases, para saber si puede cambiar el nombre de un método o no. Si nuestra clase implementa una clase del runtime de java (por ejemplo, ActionListener) e implementa el método correspondiente (actionPeformed(ActionEvent)), proguard no debería cambiar el nombre de este método. Como la anotación @Override no es obligatoria, no queda más remedio que revisar la clase padre. Por ello, debemos poner siempre el runtime de java. Las demás dependencias que tengamos, no hace falta ponerlas explícitamente en maven, ya que se obtienen de los tag dependency.
Vamos con el fichero proguard.conf. En este fichero solo tenemos que indicar que mantenga el paquete público aquellas clases que sean públicas. Y de estas clases, debe mantener los nombres de métodos y propiedades que sean publicas o protegidas. ¿Por qué protegidas? Para que si alguien hereda de etas clases públicas, tiene accesibles métodos y propiedades protegidos. El contenido del fichero es
-keep public class com.chuidiang.examples.proguard_library.shared.* -keepclassmembers class com.chuidiang.examples.proguard_library.shared.* { public protected <methods>; public protected <fields>; }
No vamos a entrar aquí en detalles de la sintaxis de este fichero de configuración, pero sí mencionar los puntos importantes.
- -keep indica clases que se deben mantener sin ofuscar el nombre de la clase y que no debe ser borradas aunque no se utilicen. Ponemos el paquete público y como admite comodines, un * para el nombre de la clase. Todas las clases/interfaces/enumerados que sean "public" en ese paquete se mantendrán. No los subpaquetes. Si quisiéramos incluir subpaquetes, podemos poner doble asterico **. Este es el motivo por el que meter todo lo que se va a usar externamente en un único paquete facilita la vida. Si no fuera así, tendríamos que poner muchas líneas de este estilo, indicando cada clase y su paquete.
- -keepclassmembers. El que la clase se mantenga con su nombre, no quiere decir que lo hagan los métodos. Proguard puede decidir borrar el método/propiedad si no se usa, o cambiarle el nombre si sí se usa. Por ello, debemos indicar que mantenga métodos y propiedades que sean publicas o protegidas (por tema de posible herencia). Al igual que antes, podemos usar un comodín * en el nombre de la clase.
Publicar el jar ofuscado[editar]
Comentamos que sería interesante tener los dos jar de la librería, el ofuscado y el no ofuscado. El primero para entregar a nuestros posibles cliente y que no puedan entender fácilmente el código descompilado. El segundo para usar durante nuestras pruebas, depuración, etc.
maven solo publica un jar, así que necesitamos la ayuda de un plugin que lo haga, en nuestro caso, usaremos build-helper-maven-plugin que permite que añadamos más artefactos para publicar, por ejemplo, nuestro jar ofuscado.
En el pom.xml, la configuración del plugin debe ir después de el de proguard para que el jar ofuscado ya esté generado (los plugin se ejecutan en el orden que están escritos en el pom.xml). La configuración sería la siguiente
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>attach-artifacts</id>
<phase>package</phase>
<goals>
<goal>attach-artifact</goal>
</goals>
<configuration>
<artifacts>
<artifact>
<file>${project.build.directory}/${project.build.finalName}-small.jar</file>
<type>jar</type>
<classifier>small</classifier>
</artifact>
</artifacts>
</configuration>
</execution>
</executions>
</plugin>
Vamos a ello:
- groupId, artifactId y version del plugin
- Durante la fase package queremos añadir un nuevo artefacto, que será el jar ofuscado.
- En configuracion, indicamos los artefactos, que será el fichero del jar ofuscado, que es de tipo jar y al que añadimos el classifier small para distinguirlo del no ofuscado. Puedes poner el classifier que más te guste. En cuanto a la ubicación del fichero jar ofuscado, usamos las propiedades de maven ${project.build.directory} que el que pusimos en outputDirectory del plugin proguard, con el nombre de fichero ${project.build.finalName}-small.jar que es el que pusimos también en outjar.
Listo, con esto, cuando hagamos mvn install o mvn deploy (si tenemos configurado el deploy, que no es el caso en este ejemplo), se publicarán en el repositorio maven correspondiente ambos jars.
El proyecto con el main[editar]
El proyecto del main poco más tiene que añadir. En el pom.xml ponemos la dependencia de la librería
<dependency>
<groupId>com.chuidiang.examples</groupId>
<artifactId>proguard-library</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>small</classifier>
<scope>compile</scope>
</dependency>
a la que pondremos el classifier small o no, según queramos el jar de la librería ofuscado o no.
La configuración del plugin proguard en el pom.xml es prácticamente igual al de la librería.
En cuanto al fichero de configuración de proguard cambia un poco, sólo nos interesa que se mantenga la clase con el main y el método main. Proguard tirará de todo lo que se necesite a partir de ahí, eliminando el resto. El fichero de configuración de proguard sería
-keep public class com.chuidiang.examples.proguard_project.Main { public static void main(java.lang.String[]); }
Aquí no usamos comodines y ponemos explícitamente el nombre de la clase (Main) con todo el paquete al que pertenece y el método concreto que nos interesa mantener.
El resultado[editar]
Las siguientes imágenes muestran el código original y el código ofuscado después de generar el jar y descompilarlo.
La siguiente imagen es la clase privada. Es una clase muy simple por lo que quizás el ofuscamiento no está suficientemente ofuscado. Simplemente fijate que el nombre de la clase ha cambiado a una "a", la variable log tambien ha cmabiado por una "a" y que el método privado ha desaparecido, llevando su código al público (optimización de proguard, no tiene sentido un método privado que sólo se usa una vez).
En la siguiente imagen, la clase pública, verás que conserva publicos y protegidos, como hemos indicado, pero borra todo lo privado que no se usa, cambiándole el nombre
Y lo comentado, esto es muy sencillo y posiblemente ves poca diferencia entre ofuscar o no, pero mira este código más complejo ofuscado....
private static void e() {
for (a a : c)
d.append(a.toString() + "\n");
b.addActionListener(new ActionListener() {
public final void actionPerformed(ActionEvent param1ActionEvent) {
a a;
(a = new a()).a = new Date();
a.b = TheMainWindow.a();
a.c = TheMainWindow.b().getText();
TheMainWindow.c().add(a);
TheMainWindow.b().setText("");
}
});
}