1

I am trying to to extend a 3rd lib code with new capabilities.

And since I only need to inject some code around one method from one class, I figured I can either:

  1. Fork the project and apply my changes (seems sloppy, and will definitely require lots of work whenever I try to upgrade to a newer version of the lib, not to mention licensing nightmare)
  2. Use [AOP] to intercept that one method from that one class inside a huge jar (seems cleaner) and inject my extra tests

In case any of you wants to know, that one class is not a spring bean and is used deep in the code, so I can not simply extend it and override/wrap that method easily, it would require at least a couple extra layers of extend&override/wrap. So AspectJ & AOP seems like the better way to go.

I managed to set my project up with some plugin to invoke ajc and weave the code with my desired jar in the -inpath param. And the only problem is that ajc seems to weave everything (or at least duplicate it);

So what I need basically is to ask AJC to simply wave that class from that jar, and not the whole jar !

kriegaex
  • 63,017
  • 15
  • 111
  • 202
Younes Regaieg
  • 4,156
  • 2
  • 21
  • 37
  • It seems to me like a simple pre/post-processing step in your build. Weave your jar dependency, the result will be a bunch of binary classes somewhere. Take the modified class you need, take the other classes from the original jar unmodified and pack them as a jar. – Nándor Előd Fekete Jan 12 '17 at 13:23
  • @NándorElődFekete my project is an OSGI bundle, and when `ajc` creates the woven classes, they are automatically packaged with in the bundle. But some of the 3d party classes do rely on other 3rd party libs (optional libs) that are used by a never reached code in my case, but are causing the OSJI container to install the bundle for missing dependencies – Younes Regaieg Jan 12 '17 at 13:36
  • I don't see OSGI changing anything in this case. Unless, of course, you don't use a headless build environment, and rely instead on Eclipse IDE for building your project. – Nándor Előd Fekete Jan 12 '17 at 13:44
  • It is as simple as using an exact pointcut which really just modifies that one class or method you want to enhance. If you can describe which class/method you want to change in which way, I can provide a more precise solution. It would also be helpful to see your aspect code. Why are you concerned about the duplicated, but unchanged classes anyway? – kriegaex Jan 12 '17 at 16:50
  • @kriegaex I am concerned about them because I do have Dynamic-Import enabled on my OSGI bundle, so if a class is to be included in my bundle jar, all referenced packages need to be available on the container in order to be able to install/activate the bundle, even those referenced by an unreachable code. And for the record my pointcut references the exact method by name, qualifier and argument list && object calling the method, it wouldn't fire for any undesired call ;-) – Younes Regaieg Jan 12 '17 at 17:49
  • Then either create a modified version of your dependency, add the modified class into your own artifact so as to make it found first by the classloader, shadowing the class from the 3rd party lib, or use load-time weaving. You have plenty of methods to achieve what you want. Maven helps you implement options 1 and 2 if you use the right plugins. – kriegaex Jan 13 '17 at 16:09

1 Answers1

3

As you have noticed, the AspectJ compiler always outputs all files found in weave dependencies (in-JARs), no matter if they are changed or not. This behaviour cannot be changed via command line, AFAIK. So you need to take care of packaging your JARs by yourself.

Here is a sample project incl. Maven POM showing you how to do that. I have chosen a rather stupid example involving Apache Commons Codec:

Sample application:

The application base64-encodes a text, decodes it again and prints both texts to console.

package de.scrum_master.app;

import org.apache.commons.codec.binary.Base64;

public class Application {
    public static void main(String[] args) throws Exception {
        String originalText = "Hello world!";
        System.out.println(originalText);
        byte[] encodedBytes = Base64.encodeBase64(originalText.getBytes());
        String decodedText = new String(Base64.decodeBase64(encodedBytes));
        System.out.println(decodedText);
    }
}

Normally the output looks like this:

Hello world!
Hello world!

No surprises here. But now we define an aspect which manipulates the results returned from the third party library, replacing each character 'o' (oh) by '0' (zero):

package de.scrum_master.aspect;

import org.apache.commons.codec.binary.Base64;

public aspect Base64Manipulator {
    byte[] around() : execution(byte[] Base64.decodeBase64(byte[])) {
        System.out.println(thisJoinPoint);
        byte[] result = proceed();
        for (int i = 0; i < result.length; i++) {
            if (result[i] == 'o')
                result[i] = '0';
        }
        return result;
    }
}

BTW, if you would just use call() instead of execution() here, there would be no need to actually weave into third party code. But anyway, you asked for it, so I am showing you how to do it.

Maven POM:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>de.scrum-master.stackoverflow</groupId>
  <artifactId>aspectj-weave-single-3rd-party-class</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.source-target.version>1.8</java.source-target.version>
    <aspectj.version>1.8.10</aspectj.version>
    <main-class>de.scrum_master.app.Application</main-class>
  </properties>

  <build>

    <pluginManagement>
      <plugins>

        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.6.0</version>
          <configuration>
            <source>${java.source-target.version}</source>
            <target>${java.source-target.version}</target>
            <!-- IMPORTANT -->
            <useIncrementalCompilation>false</useIncrementalCompilation>
          </configuration>
        </plugin>

        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>aspectj-maven-plugin</artifactId>
          <version>1.9</version>
          <configuration>
            <!--<showWeaveInfo>true</showWeaveInfo>-->
            <source>${java.source-target.version}</source>
            <target>${java.source-target.version}</target>
            <Xlint>ignore</Xlint>
            <complianceLevel>${java.source-target.version}</complianceLevel>
            <encoding>${project.build.sourceEncoding}</encoding>
            <!--<verbose>true</verbose>-->
            <!--<warn>constructorName,packageDefaultMethod,deprecation,maskedCatchBlocks,unusedLocals,unusedArguments,unusedImport</warn>-->
            <weaveDependencies>
              <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
              </dependency>
            </weaveDependencies>
          </configuration>
          <executions>
            <execution>
              <!-- IMPORTANT -->
              <phase>process-sources</phase>
              <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
              </goals>
            </execution>
          </executions>
          <dependencies>
            <dependency>
              <groupId>org.aspectj</groupId>
              <artifactId>aspectjtools</artifactId>
              <version>${aspectj.version}</version>
            </dependency>
            <dependency>
              <groupId>org.aspectj</groupId>
              <artifactId>aspectjweaver</artifactId>
              <version>${aspectj.version}</version>
            </dependency>
          </dependencies>
        </plugin>

        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>exec-maven-plugin</artifactId>
          <version>1.5.0</version>
          <configuration>
            <mainClass>${main-class}</mainClass>
          </configuration>
        </plugin>

      </plugins>
    </pluginManagement>

    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>2.5</version>
        <executions>
          <execution>
            <id>remove-unwoven</id>
            <!-- Phase 'process-classes' is in between 'compile' and 'package' -->
            <phase>process-classes</phase>
            <goals>
              <goal>clean</goal>
            </goals>
            <configuration>
              <!-- No full clean, only what is specified in 'filesets' -->
              <excludeDefaultDirectories>true</excludeDefaultDirectories>
              <filesets>
                <fileset>
                  <directory>${project.build.outputDirectory}</directory>
                  <includes>
                    <include>org/apache/commons/codec/**</include>
                    <include>META-INF/**</include>
                  </includes>
                  <excludes>
                    <exclude>**/Base64.class</exclude>
                  </excludes>
                </fileset>
              </filesets>
              <!-- Set to true if you want to see what exactly gets deleted -->
              <verbose>false</verbose>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
      </plugin>
    </plugins>

  </build>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>${aspectj.version}</version>
        <scope>runtime</scope>
      </dependency>
      <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.10</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
    </dependency>
    <dependency>
      <groupId>commons-codec</groupId>
      <artifactId>commons-codec</artifactId>
    </dependency>
  </dependencies>

  <organization>
    <name>Scrum-Master.de - Agile Project Management</name>
    <url>http://scrum-master.de</url>
  </organization>
</project>

As you can see I am using <weaveDependencies> in the AspectJ Maven plugin (which translates to -inpath for the AspectJ compiler) in combination with a special execution of the Maven Clean plugin that deletes all unneeded classes and the META-INF directory from the original JAR.

If the you run mvn clean package exec:java you see:

[INFO] ------------------------------------------------------------------------
[INFO] Building aspectj-weave-single-3rd-party-class 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
(...)
[INFO] --- aspectj-maven-plugin:1.9:compile (default) @ aspectj-weave-single-3rd-party-class ---
[INFO] Showing AJC message detail for messages of types: [error, warning, fail]
(...)
[INFO] --- maven-clean-plugin:2.5:clean (remove-unwoven) @ aspectj-weave-single-3rd-party-class ---
[INFO] Deleting C:\Users\Alexander\Documents\java-src\SO_AJ_MavenWeaveSingle3rdPartyClass\target\classes (includes = [org/apache/commons/codec/**, META-INF/**], excludes = [**/Base64.class])
(...)
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ aspectj-weave-single-3rd-party-class ---
[INFO] Building jar: C:\Users\Alexander\Documents\java-src\SO_AJ_MavenWeaveSingle3rdPartyClass\target\aspectj-weave-single-3rd-party-class-1.0-SNAPSHOT.jar
[INFO] 
[INFO] --- exec-maven-plugin:1.5.0:java (default-cli) @ aspectj-weave-single-3rd-party-class ---
Hello world!
execution(byte[] org.apache.commons.codec.binary.Base64.decodeBase64(byte[]))
Hell0 w0rld!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

And this is what my target/classes directory looks like after the build:

Directory 'target/classes'

As you can see, there is only one Apache Commons class file left which goes into the created JAR.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • First of all, thank you for the effort! Your answer shows that you definitely invested some efforts. But I ended up giving up on this approach and rewriting a couple layers in order to achieve the desired behavior. Even though I am using gradle to manage my dependencies and build, I came up with a solution similar to yours, only to figure out later that the presence of a class of that package inside my bundle prevents the OSGI container from importing the rest of the classes from that package from the original library... – Younes Regaieg Jan 14 '17 at 20:11
  • As my bundle is an addon for an ECM solution and should be pluggable on multiple versions of that solution, I thought it would be a real pain in the butt to prepare woven dependencies for each and every Version of the host software... and maintain a separate project for that. – Younes Regaieg Jan 14 '17 at 20:14
  • But as I said, your answer shows lots of effort, and would definitely work if I did not have that whole OSGi complications, so a vote up and a mark as a correct answer is well deserved in this case ! – Younes Regaieg Jan 14 '17 at 20:17
  • I have no experience with OSGi, so I am sorry for missing that part. But obviously you also just noticed when trying the same thing. Thank you for accepting the answer anyway. What have you ended up doing? Creating woven dependency JARs for each version, I suppose? – kriegaex Jan 15 '17 at 08:01
  • Thanks chief this helped me around some high level BS – Justin Dennahower Mar 15 '19 at 21:47