How to make Lombok + Gson work with Spring AOP pro

2019-07-23 15:36发布

问题:

Assuming there is a simple class Student

@Data @NoArgsConstructor @AllArgsConstructor
public class Student {
    private Integer age;
    private String name;
}

Add a logging aspect With Spring AOP in aop.xml

<aop:config>
    <aop:aspect id="log" ref="logging">
        <aop:pointcut id="selectAll" expression="execution(* com.tutorial.Student.getName(..))"/>
        <aop:before pointcut-ref="selectAll" method="beforeAdvice"/>
        <aop:after pointcut-ref="selectAll" method="afterAdvice"/>
    </aop:aspect>
</aop:config>
<bean id="student" class="com.tutorial.Student">
    <property name="name"  value="Zara" />
    <property name="age"  value="11"/>
</bean>

excluding aspects fields

public class ExcludeAspects implements ExclusionStrategy {
    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        if(f.getName().startsWith("CGLIB$"))
            return true;
        return false;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
}

main,note the output of the first bean is empty ("{}"):

public static void main(String[] args) {
   Gson gson = new GsonBuilder().setPrettyPrinting().addSerializationExclusionStrategy(new ExcludeAspects()).create();
   ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");

   //return "{}"
   Student student = (Student) context.getBean("student");
   gson.toJson(student);       

   //works fine
   Student student2 = new Student(11,"Zara");
   gson.toJson(student2);       
}

Update According to the accepted answer, unProxy works for me.

回答1:

Your code seems to imply that your aspects are working, i.e. before/after advices from your configuration get executed. If they don't, you have problems in other places. I am further assuming that

  • your aspects work as designed and you have checked that,
  • you are using Spring AOP, not AspectJ with load-time weaving,
  • somehow GSON is seeing CGLIB proxies, not the original objects underneath.

Then the problem could be that GSON - I have zero experience with it, never used it before - uses reflection in order to search for fields in the proxy class. But it will not find any as the proxy only overrides methods, but does not have fields because the latter are in the original class (parent to the proxy). If this is true, you need to configure GSON to search in the original class, not in the proxy class. Then you also would not have to exclude anything.


Update:

My educated guess above was correct.

Just because I was curious about how to get the original object from a CGLIB proxy, I looked at it in a debugger. It seems like every proxy has a public final method getTargetSource which you can invoke via reflection:

package com.tutorial;

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

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Application {
  public static void main(String[] args) throws Exception {
    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");

    Student student = (Student) context.getBean("student");
    TargetSource targetSource = (TargetSource)
      student
        .getClass()
        .getMethod("getTargetSource", null)
        .invoke(student, null);
    System.out.println(gson.toJson(targetSource.getTarget()));
 }
}

This works for me with your code, but I did not use Lombok (which you did not mention at all, I just found out when trying to compile your code!) in the mix but manually created constructors, getters and setters just to get up and running.

Besides, you do not need the ExclusionStrategy anymore.

Console log:

{
  "age": 11,
  "name": "Zara"
}

BTW, Lombok is known to cause trouble in connection with AspectJ because of class naming conflicts, see my answer here. This might also affect Spring AOP.

I think that you use an unhealthy (because incompatible) mix of technologies here, if you find a solution and do not want to end up writing custom type adapters for each bean class it will be quite hacky. If you remove Lombok, at least you can switch from Spring AOP to AspectJ with LTW in order to get rid of the proxy problem. AspectJ does not use proxies, so GSON might work better with it.


Update 2:

My first update was just a quick hack during a tea break. Not being a Spring user, I had too look up the API doc first in order to find interface Advised. It contains method getTargetSource(), i.e.:

  • We can cast our Spring bean (AOP proxy) to Advised and thus avoid ugly reflection.
  • Going one step further, we can dynamically determine if a given object is an (advised) proxy or not, i.e. if you change or deactivate your aspect, the same code will still work.
package com.tutorial;

import org.springframework.aop.framework.Advised;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Application {
  public static void main(String[] args) throws Exception {
    try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("aop.xml")) {
      Gson gson = new GsonBuilder().setPrettyPrinting().create();
      Student student = (Student) context.getBean("student");
      System.out.println(gson.toJson(unProxy(student)));
    }
 }

  public static Object unProxy(Object object) throws Exception {
    return object instanceof Advised
      ? ((Advised) object).getTargetSource().getTarget()
      : object;
  }
}

Update 3: I was curious and also installed Lombok for my IDE. Actually the sample from above does work in connection with Gson and my little unProxy(Object) method. So you are good to go. :-)



回答2:

you can use @Expose annotation to ignore aop fields. eg:

Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); String json = gson.toJson(new Book());

public class book {

    @Expose
    public String name;

    @Expose
    public int some;

    ...
}


回答3:

Implement ExclusionStrategy like:

@RequiredArgsConstructor
public class ExcludeListedClasses implements ExclusionStrategy {
    @NonNull
    private Set<Class<?>> classesToExclude;
    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return false;
    }
    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return classesToExclude.contains(clazz);
    }
}

Use like:

ExclusionStrategy es = new ExcludeListedClasses( new HashSet<Class<?>>() {{
                add(Logging.class);
    }} );

Gson gson = new GsonBuilder().setPrettyPrinting()
                .addSerializationExclusionStrategy(es).create();

There might appear other unserializable classes also, added by aspects or so. Just add those also to the set provided when constructing ExcludeListedClasses.

The difference to Arun's answer is that this way you do not need to add @Expose annotation to each class's each field where would possible be unserializable fields.

You can also use method shouldSkipField(..) in a similar way if you want to skip serialization by field name, for example.