如何使 Lombok + Gson 与 Spring AOP 代理一起工作
How to make Lombok + Gson work with Spring AOP proxies
假设有一个简单的 class Student
@Data @NoArgsConstructor @AllArgsConstructor
public class Student {
private Integer age;
private String name;
}
在 aop.xml
中使用 Spring AOP 添加日志方面
<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>
不包括方面字段
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,注意第一个 bean 的输出为空(“{}”):
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);
}
更新 根据接受的答案,unProxy
适合我。
您可以使用@Expose 注释来忽略aop 字段。
例如:
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
String json = gson.toJson(new Book());
public class book {
@Expose
public String name;
@Expose
public int some;
...
}
实施 ExclusionStrategy
如:
@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);
}
}
像这样使用:
ExclusionStrategy es = new ExcludeListedClasses( new HashSet<Class<?>>() {{
add(Logging.class);
}} );
Gson gson = new GsonBuilder().setPrettyPrinting()
.addSerializationExclusionStrategy(es).create();
可能还会出现其他不可序列化的 classes,由 aspects 左右添加。只需将它们也添加到构造 ExcludeListedClasses
.
时提供的集合中
与 Arun 的回答不同的是,这样您就不需要为每个 class 的每个可能是不可序列化字段的字段添加 @Expose
注释。
例如,如果您想通过字段名称跳过序列化,您也可以以类似的方式使用方法 shouldSkipField(..)
。
您的代码似乎暗示您的方面正在工作,即来自您的配置的 before/after 建议得到执行。如果他们不这样做,那么您在其他地方就会遇到问题。我进一步假设
- 您的方面按设计工作并且您已经检查过,
- 您正在使用 Spring AOP,而不是 load-time 编织的 AspectJ,
- GSON 以某种方式看到了 CGLIB 代理,而不是下面的原始对象。
那么问题可能是 GSON - 我对它的经验为零,以前从未使用过它 - 使用反射来搜索代理中的字段 class。但它不会找到任何因为代理只覆盖方法,但没有字段,因为后者在原始 class (代理的父级)中。如果这是真的,您需要将 GSON 配置为在原始 class 中搜索,而不是在代理 class 中搜索。那么你也不必排除任何东西。
更新:
我上面的有根据的猜测是正确的。
只是因为我对如何从 CGLIB 代理中获取原始对象感到好奇,所以我在调试器中查看了它。似乎每个代理都有一个 public final 方法 getTargetSource
,您可以通过反射调用它:
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()));
}
}
这对我来说适用于你的代码,但我没有在混合中使用 Lombok(你根本没有提到,我只是在尝试编译你的代码时发现的!),而是手动创建构造函数、getter 和二传手只是为了起床 运行.
此外,您不再需要 ExclusionStrategy
。
控制台日志:
{
"age": 11,
"name": "Zara"
}
顺便说一句,众所周知,由于 class 命名冲突,Lombok 会在与 AspectJ 的连接中引起麻烦,请参阅 my answer here。这也可能影响 Spring AOP。
我认为你在这里使用了一种不健康的(因为不兼容)技术组合,如果你找到了解决方案并且不想最终为每个 bean 编写自定义类型适配器 class 这将是非常 hacky .如果删除 Lombok,至少可以从 Spring AOP 切换到 AspectJ with LTW 以摆脱代理问题。 AspectJ 不使用代理,因此 GSON 可能会更好地使用它。
更新二:
我的第一次更新只是在茶歇期间进行的快速修改。不是 Spring 用户,我也必须先查找 API 文档才能找到接口 Advised
。它包含方法getTargetSource()
,即:
- 我们可以将 Spring bean(AOP 代理)转换为
Advised
,从而避免丑陋的反射。
- 更进一步,我们可以动态确定给定对象是否是(建议的)代理,即如果您更改或停用您的方面,相同的代码仍然有效。
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;
}
}
更新 3: 我很好奇,也为我的 IDE 安装了 Lombok。实际上,上面的示例确实与 Gson 和我的小 unProxy(Object)
方法结合使用。所以你很高兴去。 :-)
假设有一个简单的 class Student
@Data @NoArgsConstructor @AllArgsConstructor
public class Student {
private Integer age;
private String name;
}
在 aop.xml
中使用 Spring AOP 添加日志方面<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>
不包括方面字段
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,注意第一个 bean 的输出为空(“{}”):
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);
}
更新 根据接受的答案,unProxy
适合我。
您可以使用@Expose 注释来忽略aop 字段。 例如:
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); String json = gson.toJson(new Book());
public class book {
@Expose
public String name;
@Expose
public int some;
...
}
实施 ExclusionStrategy
如:
@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);
}
}
像这样使用:
ExclusionStrategy es = new ExcludeListedClasses( new HashSet<Class<?>>() {{
add(Logging.class);
}} );
Gson gson = new GsonBuilder().setPrettyPrinting()
.addSerializationExclusionStrategy(es).create();
可能还会出现其他不可序列化的 classes,由 aspects 左右添加。只需将它们也添加到构造 ExcludeListedClasses
.
与 Arun 的回答不同的是,这样您就不需要为每个 class 的每个可能是不可序列化字段的字段添加 @Expose
注释。
例如,如果您想通过字段名称跳过序列化,您也可以以类似的方式使用方法 shouldSkipField(..)
。
您的代码似乎暗示您的方面正在工作,即来自您的配置的 before/after 建议得到执行。如果他们不这样做,那么您在其他地方就会遇到问题。我进一步假设
- 您的方面按设计工作并且您已经检查过,
- 您正在使用 Spring AOP,而不是 load-time 编织的 AspectJ,
- GSON 以某种方式看到了 CGLIB 代理,而不是下面的原始对象。
那么问题可能是 GSON - 我对它的经验为零,以前从未使用过它 - 使用反射来搜索代理中的字段 class。但它不会找到任何因为代理只覆盖方法,但没有字段,因为后者在原始 class (代理的父级)中。如果这是真的,您需要将 GSON 配置为在原始 class 中搜索,而不是在代理 class 中搜索。那么你也不必排除任何东西。
更新:
我上面的有根据的猜测是正确的。
只是因为我对如何从 CGLIB 代理中获取原始对象感到好奇,所以我在调试器中查看了它。似乎每个代理都有一个 public final 方法 getTargetSource
,您可以通过反射调用它:
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()));
}
}
这对我来说适用于你的代码,但我没有在混合中使用 Lombok(你根本没有提到,我只是在尝试编译你的代码时发现的!),而是手动创建构造函数、getter 和二传手只是为了起床 运行.
此外,您不再需要 ExclusionStrategy
。
控制台日志:
{
"age": 11,
"name": "Zara"
}
顺便说一句,众所周知,由于 class 命名冲突,Lombok 会在与 AspectJ 的连接中引起麻烦,请参阅 my answer here。这也可能影响 Spring AOP。
我认为你在这里使用了一种不健康的(因为不兼容)技术组合,如果你找到了解决方案并且不想最终为每个 bean 编写自定义类型适配器 class 这将是非常 hacky .如果删除 Lombok,至少可以从 Spring AOP 切换到 AspectJ with LTW 以摆脱代理问题。 AspectJ 不使用代理,因此 GSON 可能会更好地使用它。
更新二:
我的第一次更新只是在茶歇期间进行的快速修改。不是 Spring 用户,我也必须先查找 API 文档才能找到接口 Advised
。它包含方法getTargetSource()
,即:
- 我们可以将 Spring bean(AOP 代理)转换为
Advised
,从而避免丑陋的反射。 - 更进一步,我们可以动态确定给定对象是否是(建议的)代理,即如果您更改或停用您的方面,相同的代码仍然有效。
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;
}
}
更新 3: 我很好奇,也为我的 IDE 安装了 Lombok。实际上,上面的示例确实与 Gson 和我的小 unProxy(Object)
方法结合使用。所以你很高兴去。 :-)