Feature Toggling Java Annotations
Asked Answered
J

6

6

How can I feature toggle java annotations?

Simple feature toggle:- if(toggle enabled) do x

Spring allows the use of "profiles" to toggle beans.

I use these and they are fine but I'd like to toggle annotation on field or classes.. how can I do that?

Use case, I have a class that has jpa annotations. I want to be able to mark via configuration that some fields are @transient when in certain environments.

Jylland answered 22/8, 2012 at 2:17 Comment(2)
Do not process the annotations...the metadata is only good if you process itDioscuri
I do not follow. I want the annotation to exist.. but to be able to disable them via configuration/feature toggling.Jylland
R
3

One of the possible options is to use aspectj with its ability to declare annotations and spring's ability of load-time aspect weaving.

I suppose that annotations cannot be declared conditionally, but you can always compile them in a separate jar that can be put into the classpath depending on the certain environment so that load-time weaver will be able to find it.


UPDATE

While there are a lot of useful answers here, I found disabling/enabling annotations quite interesting playing with aspectj, so the sample is below.

The latest versions of aspectj support removing of annotations, but for now this feature is only available for the field annotations, so quite useful way is not to declare annotations at all and if they have to be enabled - to put the jar with precompiled aspects which will enable the annotations into the classpath as I mentioned earlier.


SAMPLE


The first jar

The main class

package org.foo.bar;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
        MyClass myObj = context.getBean("myObj", MyClass.class);

        System.out.println(myObj);
        System.out.println(myObj.getValue1());
        System.out.println(myObj.getValue2());
    }

}

The class we will declare annotations in

package org.foo.bar;

public class MyClass {

    @MyAnn("annotated-field-1")
    private String field1;
    private String field2;

    @MyAnn("annotated-method-1")
    public String getValue1() {
        String value = null;
        try {
            MyAnn ann = getClass().getDeclaredMethod("getValue1").getAnnotation(MyAnn.class);
            if(ann != null) {
                value = ann.value();
            }
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        return value;
    }

    public String getValue2() {
        String value = null;
        try {
            MyAnn ann = getClass().getDeclaredMethod("getValue2").getAnnotation(MyAnn.class);
            if(ann != null) {
                value = ann.value();
            }
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        return value;
    }

    @Override
    public String toString() {
        String field1 = null;
        try {
            MyAnn ann = getClass().getDeclaredField("field1").getAnnotation(MyAnn.class);
            if(ann != null) {
                field1 = ann.value();
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }

        String field2 = null;
        try {
            MyAnn ann = getClass().getDeclaredField("field2").getAnnotation(MyAnn.class);
            if(ann != null) {
                field2 = ann.value();
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }

        StringBuilder sb = new StringBuilder();
        sb.append("MyClass");
        sb.append("{field1='").append(field1).append('\'');
        sb.append(", field2='").append(field2).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

The annotation itself

package org.foo.bar;

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

@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface MyAnn {

    String value();

}

Application context

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:load-time-weaver />

    <bean id="myObj" class="org.foo.bar.MyClass" />

</beans>

The second jar

The aspect

package org.foo.bar;

public aspect ToggleAnnotationAspect {

    declare @field : private String org.foo.bar.MyClass.field1 : -@MyAnn;
    declare @field : private String org.foo.bar.MyClass.field2 : @MyAnn("annotated-field-2");

    declare @method : public String org.foo.bar.MyClass.getValue2() : @MyAnn("annotated-method-2");

}

META-INF/aop.xml

<?xml version="1.0"?>

<aspectj>
    <aspects>
        <aspect name="org.foo.bar.ToggleAnnotationAspect"/>
    </aspects>
</aspectj>

Running the application without the second jar in the classpath

java -javaagent:spring-instrument-3.1.3.RELEASE.jar \
     -classpath app1.jar;<rest_of_cp> org.foo.bar.Main

will print

MyClass{field1='annotated-field-1', field2='null'}
annotated-method-1
null

Running the application with the second jar in the classpath

java -javaagent:spring-instrument-3.1.3.RELEASE.jar \
     -classpath app1.jar;app1-aspects.jar;<rest_of_cp> org.foo.bar.Main

will print

MyClass{field1='null', field2='annotated-field-2'}
annotated-method-1
annotated-method-2

So no modification to the application source were made at all.

Rosalbarosalee answered 21/11, 2012 at 5:5 Comment(0)
K
4

As mentioned before, trying to "disable" annotation, while it might be possible, isn't really the best way to approach your problem.

Like Adrian Shum said, you should change how the framework processes the annotation. In your case there should be some ORM provider underneath you JPA implementation (such as Hibernate).

Most ORMs have some way to provide custom functionality, for instance in the case of Hibernate, you could create an Interceptor and register it by adding hibernate.ejb.interceptor to the persistence-unit in your JPA configuration as detailed here .

What this interceptor should do is up to you, but I would suggest using a different annotation (such as @ConditionalTransiet) one way is to go over the fields via reflection, check if they have the annotation and if it's in the wrong enviornment then using onLoad and onSave wipe the relevant fields from the Object.

Kunming answered 21/11, 2012 at 8:6 Comment(1)
Indeed it's got nothing to do with Spring. You need to change the way the ORM handles the annotations. Using an Interceptor is a bit overkill imho. Using a simple orm.xml file should do the trick as well (see my answer below).Doe
R
3

One of the possible options is to use aspectj with its ability to declare annotations and spring's ability of load-time aspect weaving.

I suppose that annotations cannot be declared conditionally, but you can always compile them in a separate jar that can be put into the classpath depending on the certain environment so that load-time weaver will be able to find it.


UPDATE

While there are a lot of useful answers here, I found disabling/enabling annotations quite interesting playing with aspectj, so the sample is below.

The latest versions of aspectj support removing of annotations, but for now this feature is only available for the field annotations, so quite useful way is not to declare annotations at all and if they have to be enabled - to put the jar with precompiled aspects which will enable the annotations into the classpath as I mentioned earlier.


SAMPLE


The first jar

The main class

package org.foo.bar;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
        MyClass myObj = context.getBean("myObj", MyClass.class);

        System.out.println(myObj);
        System.out.println(myObj.getValue1());
        System.out.println(myObj.getValue2());
    }

}

The class we will declare annotations in

package org.foo.bar;

public class MyClass {

    @MyAnn("annotated-field-1")
    private String field1;
    private String field2;

    @MyAnn("annotated-method-1")
    public String getValue1() {
        String value = null;
        try {
            MyAnn ann = getClass().getDeclaredMethod("getValue1").getAnnotation(MyAnn.class);
            if(ann != null) {
                value = ann.value();
            }
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        return value;
    }

    public String getValue2() {
        String value = null;
        try {
            MyAnn ann = getClass().getDeclaredMethod("getValue2").getAnnotation(MyAnn.class);
            if(ann != null) {
                value = ann.value();
            }
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        return value;
    }

    @Override
    public String toString() {
        String field1 = null;
        try {
            MyAnn ann = getClass().getDeclaredField("field1").getAnnotation(MyAnn.class);
            if(ann != null) {
                field1 = ann.value();
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }

        String field2 = null;
        try {
            MyAnn ann = getClass().getDeclaredField("field2").getAnnotation(MyAnn.class);
            if(ann != null) {
                field2 = ann.value();
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }

        StringBuilder sb = new StringBuilder();
        sb.append("MyClass");
        sb.append("{field1='").append(field1).append('\'');
        sb.append(", field2='").append(field2).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

The annotation itself

package org.foo.bar;

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

@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface MyAnn {

    String value();

}

Application context

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:load-time-weaver />

    <bean id="myObj" class="org.foo.bar.MyClass" />

</beans>

The second jar

The aspect

package org.foo.bar;

public aspect ToggleAnnotationAspect {

    declare @field : private String org.foo.bar.MyClass.field1 : -@MyAnn;
    declare @field : private String org.foo.bar.MyClass.field2 : @MyAnn("annotated-field-2");

    declare @method : public String org.foo.bar.MyClass.getValue2() : @MyAnn("annotated-method-2");

}

META-INF/aop.xml

<?xml version="1.0"?>

<aspectj>
    <aspects>
        <aspect name="org.foo.bar.ToggleAnnotationAspect"/>
    </aspects>
</aspectj>

Running the application without the second jar in the classpath

java -javaagent:spring-instrument-3.1.3.RELEASE.jar \
     -classpath app1.jar;<rest_of_cp> org.foo.bar.Main

will print

MyClass{field1='annotated-field-1', field2='null'}
annotated-method-1
null

Running the application with the second jar in the classpath

java -javaagent:spring-instrument-3.1.3.RELEASE.jar \
     -classpath app1.jar;app1-aspects.jar;<rest_of_cp> org.foo.bar.Main

will print

MyClass{field1='null', field2='annotated-field-2'}
annotated-method-1
annotated-method-2

So no modification to the application source were made at all.

Rosalbarosalee answered 21/11, 2012 at 5:5 Comment(0)
F
1

I don't think it's possible on field level. What you could possibly do is exclude whole class from beeing considered by JPA (via persistence-unit config). This should be possible to be done per profile I believe.

Fourcycle answered 22/8, 2012 at 3:44 Comment(0)
E
1

No you cannot do it.

Annotation is simply a piece of meta-data. It is attached to the byte code (well, depending on the retention) after you compiled your source code. Therefore it is always there. You cannot make it disappear in runtime using normal way.

However, annotation is simply meta-data. It does nothing by its own. There should be someone else inspect the annotations, and does their work accordingly. Therefore what you should look into is, you should find some way to tell that "someone" who inspect annotation, and tell it what is the correct way to interpret the annotation (e.g. ignore some annotation etc)

There is no general way to perform this action, as it all depends on the one who are inspecting annotation.


If you insists to take the trouble way, I believe you may alter the classes on runtime. It will be a tedious work. As I remember, tools like Javassist allow you to "rewrite" classes loaded by classloader and save it back. However you are going to face a lot of issue, for example, your class alternation process should take place before any other code runs, if not, for example, Hibernate will have already inspected the unmodified classes and done its setup, and even you removed annotations from classes afterwards, it will do nothing.

Elephantine answered 21/11, 2012 at 3:28 Comment(0)
B
1

You could try this (using aspects):

@Profile("active")
privileged aspect AddField {
    private String MyClass.name;
}

@Profile("inactive")
privileged aspect AddFieldTransient {
    @Transient
    private String MyClass.name;
}

Not sure if the profile annotation works on aspect classes though. Also, this way would require you to add these aspects for every single field you want to apply this behaviour to. Its difficult to make it more generic than this.

Bechler answered 21/11, 2012 at 8:0 Comment(0)
D
1

In the beginning of Hibernate, they designed it to separate the configuration from the actual classes, using separate mapping xml files. Annotations were only added later as a compromise to convenience.

It's still possible in standardized JPA, using orm.xml configuration to override the annotations. See http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html/xml-overriding.html for a reference.

In your case, if you use metadata-complete="true", all meta-data is taken from the orm.xml file instead of from the annotations. Then you could use two different orm.xml.

<entity class="Administration" access="PROPERTY" metadata-complete="true">
    <attributes>
       <basic name="status"/>
       <basic name="optional">
     </attributes>

<entity class="Administration" access="PROPERTY" metadata-complete="true">
    <attributes>
       <basic name="status"/>
       <!-- omitted optional property -->
     </attributes>

Doe answered 26/11, 2012 at 23:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.