0

There is a lot of documentation regarding weaving into JAR files, such as the examples described in the book AspectJ in Action: Second Edition.

However, I barely see any questions regarding weaving into WAR files. Due to the popularity of WAR files and the AspectJ library, I find it hard to believe that it's not supported, and so I'm hoping for some answers.

Let's say you have a "legacy" Java project which is being packaged as a WAR file using the maven-war-plugin plugin. Keep in mind that changing it from WAR to JAR packaging is nearly impossible due to the risk involved.

Now, let's say I wanted to introduce AOP for some cross-cutting functionality. Due to the nature of our applications, my best bet is to create a separate project. So basically, the aspect classes will not be in the same project as the legacy project. It will be separate.

After this part, I'm stuck. From reading the book and the other documentation online, there seems to be two options for compile time weaving:

  1. Weave the sources directly. This can be done by creating the aspect classes directly in the legacy project, and then use aspectj-maven-plugin plugin. This is not really an option because I want my AOP logic to impact multiple projects and not just one project.
  2. Create a separate library and compile it using aspectj-maven-plugin plugin. Then create another maven project which will taken in the unwoven application and the aspect library, and weaves it together. Example taken from the book:
<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>aspectj-maven-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <weaveDependencies>
                    <weaveDependency>
                        <groupId>ajia.helloworld</groupId>
                        <artifactId>application-unwoven</artifactId>
                    </weaveDependency>
                </weaveDependencies>
                <aspectLibraries>
                    <aspectLibrary>
                        <groupId>ajia.helloworld</groupId>
                        <artifactId>profiling-aspect-library</artifactId>
                    </aspectLibrary>
                </aspectLibraries>
            </configuration>
        </plugin>
    </plugins>
</build>

The problem with the second option is that <weaveDependency> requires the application to be a JAR file. Atleast, as far as I know.

I did come across a similar question on the AspectJ mailing list: http://aspectj.2085585.n4.nabble.com/Weaving-into-a-war-file-td2082924.html. In case the link expires, the answer to the question was:

you need to take it apart, weave the .jar and put it back together. You cannot directly weave the jar within the war

But is that even possible? I don't see any documentation for that either.

There are similar questions on SO which either don't have any responses at all or are wrong/outdated.

I would appreciate any help on this since I do not know what my next step should be.

gjvatsalya
  • 1,129
  • 13
  • 29
  • 1
    I don't know definitively if you can weave a war project. In the past, I have always aggregated _pre-woven artifacts_ into the war "assembly". That's to say, I'll have the war depend on an artifact that is already woven. If you need to weave jar files you don't build as part of the same (multi module) project, create a new module "some-jar-woven" and follow the instructions here https://stackoverflow.com/questions/4853292/spring-aspectj-compile-time-weaving-external-jar, then depend on the some-jar-woven in your war assembly. – Not a JD Jun 13 '19 at 17:31
  • *If you need to weave multiple jars you don't own, you _can_ do it in "some-jar-woven". Might make sense to call the module "some-jars-woven". You lose clarity of jar separation in the war file contents tho (i.e. previously you had artifact1.jar and artifact2.jar, now you would have artifacts1and2.jar etc etc) – Not a JD Jun 13 '19 at 17:33
  • @NotaJD Just so I understand your comment correctly - are you saying that you first weave your application, and then build it as a JAR? This mean that, in my case, I would need to build my application as a JAR (instead of WAR), weave it, and then make it into a WAR? If so, I'm just really hoping to avoid that since the risk of changing the packaging is high and can have a massive negative impact. – gjvatsalya Jun 13 '19 at 17:44
  • 1
    I understand your concern! Typically - in projects I work in - the war module is an aggregator of other modules. That's to say, it does not contain any _code_ that is compiled (although it might sometimes contain _configuration_ that is enriched by maven property substitution etc). With that in mind, I weave the modules that I aggregate into the war, that I need to apply cross-cutting concerns to. If your "application" is a single war project, then I don't think there's much you can do other than split it into a jar that is depended upon by the war, and then weave the jar. – Not a JD Jun 13 '19 at 17:49
  • 1
    @NotaJD, I upvoted two of your comments because they nicely explain the correct way of doing this without any hassle. The OP would get exactly what he wants. But the information is prose only. I think your knowledge is worth to be cast into an answer with some sample POMs. I could write that answer but I want you to (a) finish the good job you started and (b) get rewarded by the OP accepting & upvoting your answer. – kriegaex Jun 14 '19 at 00:40
  • @kriegaex thank you sir! I will indeed do that during lunch tomorrow, great idea. – Not a JD Jun 14 '19 at 01:39
  • Yep, I would definitely appreciate examples. I'm even trying to put the Aspect code in the same project (although in a separate maven module), but I'm still not able to get it to work. So there must be something semi-obvious that I'm missing. – gjvatsalya Jun 14 '19 at 14:25
  • I'll start answering this now :) – Not a JD Jun 14 '19 at 19:10
  • I actually just figured it out a few minutes ago (although I'm running into other potentially unrelated errors). But I'm still curious about your answer just in case there's a better way. – gjvatsalya Jun 14 '19 at 19:12

1 Answers1

1

I'll try and present two distinct solutions and then you can pick/choose or mix/match how you go about implementing your solution.


Example 1: You have been asked to dump some text to stdout pre- and post-execution of the org.apache.log4j.Logger.info(Object o) method found in log4j.

(This is a very contrived example, but I'm using it because I have been asked to do something not entirely dissimilar in the past!)

I'll assume you're using a Multi-Module Maven Project.

To achieve this, we can consume the log4j jar (compiled classes), apply an Aspect to them, and spit out a new set of load-time-woven compiled classes. We do this by leveraging the weaveDependency functionality you've already called out in a module of its own. So, instead of depending on log4j in your projects, you'll depend on ltwjar-enhanced-log4j as described below.

With that in mind, create your project hierarchy thus:

ltwjar
      /ltwjar-ltwjar-enhanced-log4j
      /ltwjar-runtime

Setup your parent pom.xml to look something like this (we set a couple properties here just for convenience/consistency's sake - there are other ways to control versioning such as dependencyManagement and imported depdendencies, but that's for a different question!):

<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>com.example</groupId>
    <artifactId>ltwjar</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <aspectj.version>1.8.13</aspectj.version>
        <log4j.version>1.2.17</log4j.version>
    </properties>

    <modules>
        <module>ltwjar-enhanced-log4j</module>
        <module>ltwjar-runtime</module>
    </modules>
</project>

Set up your ltwjar-enhanced-log4j pom.xml like this:

<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>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>ltwjar</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>ltwjar-enhanced-log4j</artifactId>

    <dependencies>
        <!-- we need this because the post-weave classes may (will) depend on aspectj types -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${aspectj.version}</version>
        </dependency>

        <!-- ATTENTION! Scope this to provided otherwise it won't work - because of transitive dependencies,
        anything that depends on this module will end up getting the _actual_ log4j classes versus the ones we are enriching! -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.11</version>
                <configuration>
                    <!-- instruct aspectj to weave the classes in the jar! -->
                    <weaveDependencies>
                        <weaveDependency>
                            <groupId>log4j</groupId>
                            <artifactId>log4j</artifactId>
                        </weaveDependency>
                    </weaveDependencies>
                    <Xlint>warning</Xlint>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

And now let's setup our stub "runtime" module to leverage the load-time-wove log4j ltw-enhanced-log4j when it logs by configuring its pom.xml thus:

<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>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>ltwjar</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>ltwjar-runtime</artifactId>
  <dependencies>
    <!-- Note that this module doesn't care (itself) about aspectj.
         It _does_ care that it depends on the ltwjar-enhanced-log4j module
         and not log4j, however. So let's depend on that! -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>ltwjar-enhanced-log4j</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>
</project>

OK that's our framework setup. Now let's create a pointless aspect to demonstrate the point!

In ltwjar-enhanced-log4j create type com.example.aspect.LoggingAspect like so:

package com.example.aspect;

import org.apache.log4j.Logger;

public aspect LoggingAspect {
    // this pointcut will match "info(Object o)" method executions where the
    // target is an instanceof Logger
    pointcut logInfoWithObject(Object obj, Logger logger) :
         execution(void info(Object)) && args(obj) && target(logger);

    // here is our advice - simply sysout something before the execution
    // proceeds and after it has finished - regardless of outcome (exception
    // or not).
    void around(Object obj, Logger logger) : logInfoWithObject(obj, logger) {
        System.out.println("Before Logger.info(Object o)");

        try {
            proceed(obj, logger);
        } finally {
            System.out.println("After Logger.info(Object o)");
        }
    }
}

And finally in ltwjar-runtime, create a driver/harness that shows everything working. Type com.example.Driver:

package com.example;

import org.apache.log4j.Logger;

public class Driver {
    private static final Logger LOG = Logger.getLogger(Driver.class);

    public static void main(String[] args) {
        LOG.info("In main");
    }
}

Now from your parent project run mvn clean install and then mvn -pl ltwjar-runtime exec:java -Dexec.mainClass="com.example.Driver".

The output should be something like this (since you don't have a log4j.xml on your classpath):

Before Logger.info(Object o)
log4j:WARN No appenders could be found for logger (com.example.Driver).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
After Logger.info(Object o)

Note the first and last lines.

What's happening here:

  1. In ltwjar-enhanced-log4j we "enhance" log4j by wrapping an advice around Logger.info(Object o)
  2. We repackage log4j.jar into a new jar ltwjar-enhanced-log4j.jar that contains the woven type
  3. We depend on ltwjar-enhanced-log4j.jar instead of log4j in our application code...
  4. ...And in doing so, the advice sticks

OK so how does this fit into the warfile that was asked about originally?

Simple - don't put any code in your warfile module. If you have to put something in there, make it configuration. Instead, move all your code to a yourproject-domain or yourproject-logic or ... (insert module name here) and have your warfile depend on that module. This is good practice since it enables a Separation of Concerns: your application shouldn't know it's running under a warfile umbrella; it could just as easily be running as a Spring Boot tomcat uberwar etc. The packaging concern (war) is distinct from the business concern (domain).

I'll update with another example of shared aspect library this evening, unless someone beats me to it. Also, if some of this makes no sense, holler in comments!

Not a JD
  • 1,864
  • 6
  • 14