Instead of updating my first answer here yet another time after you massively edited your question, I have decided to write a new answer for the situation you now describe. As I said, your prose does not constitute a valid MCVE, so I need to make a few educated guesses here.
To anyone reading this answer: Please read the other one first, I don't want to repeat myself even though there is redundancy in between the two answers with respect to code and Maven configuration.
The situation to me looks like this according to your description:
Bean marker annotation:
package de.scrum_master.app;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(TYPE)
public @interface Bean {}
Some POJOs, two of them @Bean
s, one not:
package de.scrum_master.app;
@Bean
public class Resource {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
package de.scrum_master.app;
@Bean
public class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
@Override
public String toString() {
return "Person[firstName=" + firstName + ", lastName=" + lastName + ", age=" + age + "]";
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package de.scrum_master.app;
public class NoBeanResource {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Database storage interface each @Bean
class should implement:
I had to invent some fake methods here because you did not tell me what the interface and its implementation really look like.
package de.scrum_master.app;
public interface StoredOnDatabase {
void writeToDatabase();
void readFromDatabase();
}
Aspect introducing methods to the Resource
class:
This is the same as in my first answer and described there, nothing to add here, just repeating the code:
package de.scrum_master.aspect;
import de.scrum_master.app.Resource;
public aspect MethodIntroducer {
public Resource.new(String id) {
this();
setId(id);
}
public boolean Resource.equals(Object obj) {
if (!(obj instanceof Resource))
return false;
return getId().equals(((Resource) obj).getId());
}
public String Resource.toString() {
return "Resource[id=" + getId() + "]";
}
}
Aspect intercepting setter method calls:
package de.scrum_master.aspect;
import de.scrum_master.app.Bean;
public aspect BeanSetterInterceptor {
before(Object newValue) : @within(Bean) && execution(public void set*(*)) && args(newValue) {
System.out.println(thisJoinPoint + " -> " + newValue);
}
}
The aspect prints something like this when setter methods are being executed:
execution(void de.scrum_master.app.Resource.setId(String)) -> dummy
execution(void de.scrum_master.app.Resource.setId(String)) -> A
execution(void de.scrum_master.app.Resource.setId(String)) -> B
execution(void de.scrum_master.app.Person.setFirstName(String)) -> Jim
execution(void de.scrum_master.app.Person.setLastName(String)) -> Nobody
execution(void de.scrum_master.app.Person.setAge(int)) -> 99
BTW, you could alternatively also directly intercept field write access via set()
pointcut instead of indirectly intercepting setter methods by name. How you do it depends on what you want to achieve and if you want to stay on API level (public methods) or also track internal field assignments done in-/outside of setter methods.
Aspect making @Bean
s implement the StoredOnDatabase
interface:
Firstly, the aspect provides method implementations for the interface. Secondly it declares that all @Bean
classes should implement this interface (and also inherit method implementations). Please note how AspectJ can directly declare method implementations on interfaces. It could even declare fields. This also worked before there were interface default methods in Java. There is no need to declare a class implementing the interface and overriding interface methods as an intermediary, it works directly on the interface!
package de.scrum_master.aspect;
import de.scrum_master.app.StoredOnDatabase;
import de.scrum_master.app.Bean;
public aspect DatabaseStorageAspect {
public void StoredOnDatabase.writeToDatabase() {
System.out.println("Writing " + this + " to database");
}
public void StoredOnDatabase.readFromDatabase() {
System.out.println("Reading " + this + " from database");
}
declare parents: @Bean * implements StoredOnDatabase;
}
JUnit test demonstrating all the aspect-introduced features:
Please note that the classes above just use System.out.println()
, no logging framework. Thus the test uses System.setOut(*)
for injecting a Mockito mock in order to verify the expected logging behaviour.
package de.scrum_master.app;
import org.junit.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.io.PrintStream;
public class BeanAspectsTest {
private PrintStream systemOut;
@Before
public void doBefore() {
systemOut = System.out;
System.setOut(mock(PrintStream.class));
}
@After
public void doAfter() {
System.setOut(systemOut);
}
@Test
public void canCallConstructorWithArgument() {
// Awkward way of verifying that no exception is thrown when calling this
// aspect-introduced constructor not present in the original class
assertNotEquals(null, new Resource("dummy"));
}
@Test
public void testToString() {
assertEquals("Resource[id=dummy]", new Resource("dummy").toString());
}
@Test
public void testEquals() {
assertEquals(new Resource("A"), new Resource("A"));
assertNotEquals(new Resource("A"), new Resource("B"));
// BeanSetterInterceptor should fire 4x because MethodIntroducer calls 'setId(*)' from
// ITD constructor. I.e. one aspect can intercept methods or constructors introduced
// by another one! :-)
verify(System.out, times(4)).println(anyString());
}
@Test
public void testPerson() {
Person person = new Person("John", "Doe", 30);
person.setFirstName("Jim");
person.setLastName("Nobody");
person.setAge(99);
// BeanSetterInterceptor should fire 3x
verify(System.out, times(3)).println(anyString());
}
@Test
public void testNoBeanResource() {
NoBeanResource noBeanResource = new NoBeanResource();
noBeanResource.setId("xxx");
// BeanSetterInterceptor should not fire because NoBeanResource has no @Bean annotation
verify(System.out, times(0)).println(anyString());
}
@Test
public void testDatabaseStorage() {
// DatabaseStorageAspect makes Resource implement interface StoredOnDatabase
StoredOnDatabase resource = (StoredOnDatabase) new Resource("dummy");
resource.writeToDatabase();
resource.readFromDatabase();
// DatabaseStorageAspect makes Person implement interface StoredOnDatabase
StoredOnDatabase person = (StoredOnDatabase) new Person("John", "Doe", 30);
person.writeToDatabase();
person.readFromDatabase();
// DatabaseStorageAspect does not affect non-@Bean class NoBeanResource
assertFalse(new NoBeanResource() instanceof StoredOnDatabase);
// We should have 2x2 log lines for StoredOnDatabase method calls
// plus 1 log line for setter called from Resource constructor
verify(System.out, times(5)).println(anyString());
}
}
Maven POM:
This is almost the same as in the first answer, I just added Mockito.
<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-itd-example-57525767</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.source-target.version>8</java.source-target.version>
<aspectj.version>1.9.4</aspectj.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</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.11</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>-->
</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>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>