Hot questions for Using AspectJ in jpa

Question:

I just finished reading AspectJ in Action and am trying to write some simple aspects to start with. I would like to write an aspect that will generate a compile-time warning for fields that use EnumType.ORDINAL to persist to the database, but not for those that use EnumType.STRING. I've written similar aspects, but this is the first one I'm trying that uses an annotation in the pointcut, and I'm doing something wrong.

I have a JPA2.1 entity like the one shown below, and would like the @Enumerated(EnumType.ORDINAL) annotation attached to myEnumFieldB to generate a compiler warning...

import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.EnumType;

@Entity
public class myEntity {
    @Enumerated(EnumType.STRING) // I want this to compile ok
    protected MyEnumType myEnumFieldA;

    @Enumerated(EnumType.ORDINAL) // I want this to throw a warning
    protected MyEnumType myEnumFieldB;

    // primary key, other fields, getters & setters, etc. omitted
}

...and here's a copy of my code trying several different pointcuts (commented out), with the error messages included beside them. The named pointcut is only used by the last commented-out line.

import javax.persistence.Enumerated;
import javax.persistence.EnumType;

public aspect DetectEnumPersistencePolicy {
    pointcut ordinalEnumPersistence(Enumerated enumerated)
        : @annotation(enumerated) && if(enumerated.value() == EnumType.ORDINAL);
        // used below in a commented-out pointcut

    declare warning
        : @Enumerated(EnumType.ORDINAL)
        //ERROR: Syntax error on token "Enumerated", "pointcut name" expected

        //: @javax.persistence.Enumerated(javax.persistence.EnumType.ORDINAL)
        //ERROR: Syntax error on token "javax", "pointcut name" expected

        //: @Enumerated(EnumType.ORDINAL) * *.*
        //ERROR: Syntax error on token "Enumerated", "pointcut name" expected

        //: execution(@Enumerated(EnumType.ORDINAL) * *.*)
        //ERROR: Syntax error on token ")", "(" expected

        //: ordinalEnumPersistence(enumerated)
        // ERROR: if() pointcut designator cannot be used in declare statement

        : "Please consider using string persistence of enumerated types instead.";
}

My thoughts so far are:

  1. When I tried including if(enumerated.value() == EnumType.ORDINAL) the error message was if() pointcut designator cannot be used in declare statement. This made me think that I can't use a pointcut with an if() because that would be evaluated at runtime but the warning needs to be generated at compile time (even though the data is all there at compile time because the annotation's value doesn't vary with the runtime state)?
  2. Same for execution(@Enumerated(EnumType.ORDINAL) * *.*) because execution occurs at runtime. The error message Syntax error on token ")", "(" expected didn't mean much to me (it's referring to the second )).
  3. If I just use @annotation(enumerated) it should work at compile-time, but then I don't know how to perform the check enumerated.value() == EnumType.ORDINAL before throwing the warning?
  4. I've tried using the fully qualified names in the pointcut @javax.persistence.Enumerated(javax.persistence.EnumType.ORDINAL) but still got the error message Syntax error on token "javax", "pointcut name" expected.
  5. I tried appending some asterisks to the annotation in the pointcut to specify that I'm referring to a field @Enumerated(EnumType.ORDINAL) * *.* but still got the error message Syntax error on token "Enumerated", "pointcut name" expected.

For completeness, I'm working in Spring Tool Suite 3.7.2 with Java 1.8 and using Maven to manage the following AspectJ dependencies:

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjlib</artifactId>
  <version>1.6.2</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.8.7</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjtools</artifactId>
  <version>1.8.7</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.7</version>
</dependency>

Any help would be appreciated - I can't see why @Enumerated(EnumType.ORDINAL) doesn't work?


Answer:

Actually you have a basic problem: There is no pointcut catching member declarations, only for read/write access on members. I am talking about get() and set(), respectively. Consequently, what you want to intercept are those. Try this:

Dummy helper class to make the sample code compile:

package de.scrum_master.app;

public class MyEnumType {}

Java class with annotated members plus main method demonstrating the aspect:

package de.scrum_master.app;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;

@Entity
public class MyEntity {
    @Enumerated(EnumType.STRING) // I want this to compile ok
    protected MyEnumType myEnumFieldA;

    @Enumerated(EnumType.ORDINAL) // I want this to throw a warning
    protected MyEnumType myEnumFieldB;

    public static void main(String[] args) {
        MyEntity myEntity = new MyEntity();
        myEntity.myEnumFieldA = new MyEnumType();
        System.out.println(myEntity.myEnumFieldB);
    }
}

As you can see, myEnumFieldB is accessed once in the last line. This should throw a compiler warning.

Aspect:

package de.scrum_master.aspect;

import javax.persistence.Enumerated;
import javax.persistence.EnumType;

public aspect DetectEnumPersistencePolicy {
    pointcut ordinalEnumPersistence() :
        set(@Enumerated(value=EnumType.ORDINAL) * *) ||
        get(@Enumerated(value=EnumType.ORDINAL) * *);

    declare warning : ordinalEnumPersistence() :
        "Please consider using string persistence of enumerated types instead.";
}

This is what you will see in Eclipse:

Question:

My problem is related to localization, I have JPA entities having fields like fieldnameEN,fieldnameFR,fieldnameRU. I want to write some getter method which automatically detects current locale and returns appropriate field. I'm using facelets and such getter will help me to call getter and delegate localization issues to backend. Locale already stored in session and getting it is not a problem. I know how do such method manually, something like

getProperty(){
locale = takeFromSession();
if(locale=en) return getPropertyEN();
if(locale=fr) return getPropertyFR();
}

But I want to keep DRY principle with the help of AspectJ or some interceptor.

Current thoughts on implementation - in each getter call determine running method name, pass Object state to some interceptor and in interceptor perform locale checks and return appropriate value of field which is already passed to interceptor.

Is there any working example of solving such problem?

How pass object state to interceptor?

Are there any better approaches to solve my problem?

UPDATE

Kriegaex recommended to use bundles. Actually, we use bundles for markup (headers and captions) but we also need to localize entities stored in database. Bundles require usage of 'hash tags' as keys in .property files, but I don't want to store entity values as hash tags. Bundles will force users to fill business values as 'hash tags' and I wouldn't like that)

Even if use english values as key or some hash from values we need to make 100 queries if entity has 100 properties. And yes, I mean 100 DB queries, because AFAIK bundle stored in RAM which may be insufficient to store translations that's why in our case bundles should be in key-value DB.

About recompilation - most probably we will have only 3 languages and don't need scale in such direction.

If somebody know answer to my topic question, please share some little example)


Answer:

AspectJ is not intended to be used to patch up bad application design. I could easily tell you how to write some cheap aspect code using reflection in order to call the right getter for the current language, but

  • it is ugly,
  • it is slow,
  • having one property + getter per language and encoding the language ID in the method name does not scale. If you want to add another language, you will have to add fields to dozens or hundreds of entities.

Maybe you should consider using a standard means like resource bundles for your property names. This way you can change text constants or even add new languages without recompiling the code. Because internationalisation is a cross-cutting concern, you can then still use AspectJ in order to declare access methods for your translations via ITD (inter-type definition) or by some other means, if you want to keep them out of the core code. That way your core code could be totally language-agnostic.


Update:

Anyway, if you want it so much, here is a sample showing you what you can do with AOP, namely with AspectJ. The solution proposed by user gknicker is similar, but it only works for one class. Mine keeps the code separate in an aspect and can apply it to many classes at once.

The plan is to manually annotate each entity class containing multi-language field captions with a marker annotation. I made up one called @Entity. Alternatively, you could also determine the target classes by their superclass or by a class or package name pattern, AspectJ is very powerful in this regard. As I said, it is just an example.

In the next step we will define an aspect which does the following:

  • Define an interface LocalisedCaption.
  • Define a few sample default methods using reflection magic in order to
    • get the localised caption for one field,
    • get all localised captions for all defined entity fields,
    • get a map of localised captions and field values for an entity instance.
  • Use ITD (inter-type declaration) in order to make all @Entity classes implement that interface and thus inherit its methods.

Last, but not least, we will use the new methods in from sample application.

package de.scrum_master.app;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity {}
package de.scrum_master.app;

@Entity
public class Person {
    public static final String firstNameEN = "first name";
    public static final String firstNameFR = "prénom";
    public static final String firstNameRU = "и́мя";

    public static final String lastNameEN = "last name";
    public static final String lastNameFR = "nom de famille";
    public static final String lastNameRU = "фами́лия";

    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + firstName + ", lastName=" + lastName + "]";
    }
}
package de.scrum_master.aspect;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import de.scrum_master.app.Application;
import de.scrum_master.app.Entity;

public aspect EntityCaptionLocaliser {
    public interface LocalisedCaption {
        String getCaption(String attributeName);
    }

    declare parents :
        @Entity * implements LocalisedCaption;

    public String LocalisedCaption.getCaption(String attributeName)
        throws ReflectiveOperationException
    {
        String fieldName = attributeName + Application.locale;
        Field field = getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return (String) field.get(this);
    }

    public Map<String, String> LocalisedCaption.getAllCaptions()
            throws ReflectiveOperationException
        {
            Map<String, String> captions = new HashMap<>();
            for (Field field : getClass().getDeclaredFields()) {
                if (Modifier.isStatic(field.getModifiers()))
                    continue;
                String attributeName = field.getName();
                captions.put(attributeName, getCaption(attributeName));
            }
            return captions;
        }

    public Map<String, Object> LocalisedCaption.getCaptionValuePairs()
            throws ReflectiveOperationException
        {
            Map<String, Object> captions = new HashMap<>();
            for (Field field : getClass().getDeclaredFields()) {
                if (Modifier.isStatic(field.getModifiers()))
                    continue;
                field.setAccessible(true);
                String attributeName = field.getName();
                captions.put(getCaption(attributeName), field.get(this));
            }
            return captions;
        }
}
package de.scrum_master.app;

public class Application {
    public static String locale = "EN";

    public static void main(String[] args) throws Exception {
        Person albert = new Person("Albert", "Einstein");
        System.out.println("Showing localised captions for " + albert + ":");
        locale = "EN";
        System.out.println(albert.getAllCaptions());
        System.out.println(albert.getCaptionValuePairs());
        locale = "FR";
        System.out.println(albert.getAllCaptions());
        System.out.println(albert.getCaptionValuePairs());
        locale = "RU";
        System.out.println(albert.getAllCaptions());
        System.out.println(albert.getCaptionValuePairs());
    }
}

Console output for Application.main:

Showing localised captions for Person [firstName=Albert, lastName=Einstein]:
{lastName=last name, firstName=first name}
{first name=Albert, last name=Einstein}
{lastName=nom de famille, firstName=prénom}
{nom de famille=Einstein, prénom=Albert}
{lastName=фами́лия, firstName=и́мя}
{фами́лия=Einstein, и́мя=Albert}

Question:

I am trying aspectj with EntityManager, its giving error

org.springframework.beans.factory.BeanCreationException:
  Error creating bean with name
  'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration',
  Add a provider like Hibernate Validator (RI) to your classpath.

POM:

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.6.12</version>
            <scope>runtime</scope>
            <!-- <version>1.8.0</version> -->
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <!-- <version>1.8.0</version> -->
        </dependency>

        <dependency>
            <groupId>com.jcabi</groupId>
            <artifactId>jcabi-aspects</artifactId>
            <version>0.22.5</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>sqljdbc4</artifactId>
            <version>4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.jcabi</groupId>
                <artifactId>jcabi-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>ajc</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

Driver application:

@SpringBootApplication
@ComponentScan(basePackages = "aspect.*")
@EnableAspectJAutoProxy
@Configuration
@EntityScan(basePackages ="aspect.*")
public class Application{

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);

        try {
            Service service=context.getBean(Service.class);
            service.save();

        } catch (RuntimeException e) {
            System.out.println("Caught runtime exception");
        }
    }
}

Service class:

@Component
public class Service{

    @Autowired
    EntityManager entityManager;

    @Transactional
    public void save() {
        System.out.println("Before Invocation");
        entityManager.persist(new Employee("009", "sunitha", "Lead"));
        System.out.println("after persist call");
}

Aspect:

   @Aspect
   @Component
   public class AuditAspect {

       @Around("execution(* javax.persistence.EntityManager.persist(..)) &&  @annotation(loggable)")
    public Object aroundServce(ProceedingJoinPoint  joinPoint, Loggable loggable) throws Throwable{
    long start = System.currentTimeMillis();
      //  System.out.println("start--> "+start);
        Object result = joinPoint.proceed();
        Logger.info(this, "info %s just called", MethodSignature.class.cast(joinPoint.getSignature()).getMethod().getName());
        return result;
    }

    }

application.properties:

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

Need to know where am doing wrong??


Answer:

As the message says ... it needs a Bean Validation API (JSR 303) provider. If you put a provider like Hibernate-Validator or Apache BVAL in your CLASSPATH I'd expect the message to go away.