本项目是一个java注解处理器(annotation processor)。可以基于已有的类自动生成一个新的类。这里生成的是源码,而非字节码。生成的源码会被jdk当一般源码对待,进入正常的编译流程。
注解处理器是支持增量编译的。当jdk发现原类,原类的基类,配置类,配置类的基类源码有改动,将会重新生成生成类。
该项目最常见的用途是自动生成DTO(Data Transfer Object)。设想以下4个使用场景:
-
有一个非常大的对象,内部属性繁多,根据业务需要向客户端返回这个对象的部分信息。 如果直接返回原对象,就会浪费很多带宽。这时更好的办法是建一个新类,仅保留其中需要的属性,即DTO。 这个任务虽然简单,但重复又繁琐,还维护困难。非常适合自动化。这正合适本项目大展身手。
-
同样是希望序列化数据返回给客户端的场景。 即使需要的是原对象所有或几乎全部的信息,但若其中有循环引用,这种对象序列化起来就会很麻烦。 虽然各个主流JSON库都有一些配置来解决循环引用的问题,但往往效果都不是很好。 举个例子,类A有属性List<B> bList,类B有属性A a,这里可能产生两种业务需求。
- 以A为主,剔除bList中的B对象的a属性;
- 以B为主,剔除a的bList属性,或者保留a,但需要剔除a的bList中的B对象的a属性。
这些个需求只能靠DTO,靠配置JSON是很难实现的。
-
一个原始对象,需要被多个服务序列化,但每个服务需要的数据形状都有细微差别。 这就不得不为每个服务的序列化过程分别做定制,这种办法可扩展性差,代码可读性也差。 若是为每个服务分别定制DTO,既可以解耦,也提高了代码可读性。就是维护麻烦。 但使用本项目后,维护也不再是问题。
-
如果使用了jpa,希望查询部分字段而非整个实体,这时就必须使用jpql或原生sql来指定查询特定字段。 而为了放置查询结果,就需要构建一个新类,即DTO。除了这个DTO的构建可以交给机器自动化外, 相关的查询语句中select部分也是可以交给机器自动化的,而本工具的jpa插件正好提供了这一功能。
Jdk 1.8+ (包含 jdk 1.8)
如果你使用maven, 先添加运行时依赖:
<dependencies>
<dependency>
<groupId>io.github.vipcxj</groupId>
<artifactId>beanknife-runtime</artifactId>
<version>${beanknife.version}</version>
</dependency>
</dependencies>
然后是配置注解处理器:
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<!-- 其他注解处理器,比如lombok -->
<path>
<groupId>io.github.vipcxj</groupId>
<artifactId>beanknife-core</artifactId>
<version>${beanknife.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
然后就可以开始使用了。下面是个最简单的例子:
import io.github.vipcxj.beanknife.runtime.annotations.ViewOf;
@ViewOf(includePattern=".*") // (1)
public class SimpleBean {
private String a;
private Integer b;
private long c;
public String getA() {
return a;
}
public Integer getB() {
return b;
}
public long getC() {
return c;
}
}
假设已经存在一个类SimpleBean
,你需要基于它生成一个新的类。新类有原类所有的属性。
你唯一需要做的就是为SimpleBean
加上ViewOf
注解,并使用includePattern
将所有属性都包括进来。
于是在下一次编译发生前,jdk会自动生成一个新类SimpleBeanView
的源文件。
SimpleBeanView
是生成类默认的名字,它默认位于原类相同的包下。当然这一切都是可以配置的。
新类SimpleBeanView
源码文件的位置根据构建工具的不同可能也不同,
若想要找到它,最简单的办法是在源码某处使用SimpleBeanView
,然后使用IDE的定位功能,直接跳转到源码。
若你发现SimpleBeanView
还不能使用,说明它还未被生成,手动编译一下即可。
BeanKnife是支持增量编译的,所以你没必要为了生成类,而特意去clean。
最终生成的SimpleBeanView
大致长成下面那样:
@GeneratedView(targetClass = SimpleBean.class, configClass = SimpleBean.class)
public class SimpleBeanView {
private String a;
private Integer b;
private long c;
// 空构造函数
public SimpleBeanView() { }
// 字段构造函数,JPQL语句中很有用
public SimpleBeanView(
String a,
Integer b,
long c
) {
this.a = a;
this.b = b;
this.c = c;
}
// copy构造函数
public SimpleBeanView(SimpleBeanView source) {
this.a = source.a;
this.b = source.b;
this.c = source.c;
}
// reader构造函数,即接受原类,转化为新类
public SimpleBeanView(SimpleBean source) {
if (source == null) {
throw new NullPointerException("The input source argument of the read constructor of class io.github.vipcxj.beanknife.cases.beans.SimpleBeanView should not be null.");
}
this.a = source.getA();
this.b = source.getB();
this.c = source.getC();
}
// read方法,将原类转为新类的静态方法
public static SimpleBeanView read(SimpleBean source) {
if (source == null) {
return null;
}
return new SimpleBeanView(source);
}
// 数组版本的read方法
public static SimpleBeanView[] read(SimpleBean[] sources) {
if (sources == null) {
return null;
}
SimpleBeanView[] results = new SimpleBeanView[sources.length];
for (int i = 0; i < sources.length; ++i) {
results[i] = read(sources[i]);
}
return results;
}
// List版本的read方法
public static List<SimpleBeanView> read(List<SimpleBean> sources) {
if (sources == null) {
return null;
}
List<SimpleBeanView> results = new ArrayList<>();
for (SimpleBean source : sources) {
results.add(read(source));
}
return results;
}
// Set版本的read方法
public static Set<SimpleBeanView> read(Set<SimpleBean> sources) {
if (sources == null) {
return null;
}
Set<SimpleBeanView> results = new HashSet<>();
for (SimpleBean source : sources) {
results.add(read(source));
}
return results;
}
// Stack版本的read方法
public static Stack<SimpleBeanView> read(Stack<SimpleBean> sources) {
if (sources == null) {
return null;
}
Stack<SimpleBeanView> results = new Stack<>();
for (SimpleBean source : sources) {
results.add(read(source));
}
return results;
}
// Map版本的read方法
public static <K> Map<K, SimpleBeanView> read(Map<K, SimpleBean> sources) {
if (sources == null) {
return null;
}
Map<K, SimpleBeanView> results = new HashMap<>();
for (Map.Entry<K, SimpleBean> source : sources.entrySet()) {
results.put(source.getKey(), read(source.getValue()));
}
return results;
}
// 以下是getter函数。默认只生成getter,而不生产setter,当然是可以通过配置来改变默认行为的
public String getA() {
return this.a;
}
public Integer getB() {
return this.b;
}
public long getC() {
return this.c;
}
}
可以看到即使只有十来行的简单类,也能生成那么一大堆东西,以后随着功能的演进,可能还会增加更多东西。 全部自己手写,何其麻烦。
注意
在上面这个例子中,直接将@ViewOf
放在了原类上,这仅仅是为了简单起见。
推荐使用配置类(下面会介绍)来进行配置,这样一方面不再有侵入性,
另一方面利用配置继承特性,可以实现几乎全局的默认配置修改。
比如BeanKnife默认不生成Setter函数,但一些人可能并不喜欢这样。
在基类上使用@ViewSetter
注解,然后所有其他配置类都继承这个基类,就可以实现全局生成Setter函数。
Beanknife的代码生成完全是围绕属性这一概念来的。 对于一个类A,可用属性的提取规则如下:
- A的所有字段被认为是属性,属性名为字段名,称为字段属性
- A的所有getter方法被认为是属性,属性名为getter方法根据Javabean规则对应的字段名(该字段不一定需要实际存在), 称为getter属性
- 若存在同名属性,getter属性覆盖字段属性。 特殊的,若字段属性是可见的(比如是public的),但getter属性不可见(比如是private的),getter属性覆盖字段属性后,将导致该属性不可见。
- 字段属性和getter属性合称基础属性,因为它们都基于原类。
下面举个例子进行讲解
class Bean {
private String a; // (1)
public int b; // (2)
protected long c; // (3)
public short d; // (4)
public String getA() { return this.a; } // (5)
private short getD() { return this.d; } // (6)
}
- 可见性为
private
的字段属性a
- 可见性为
public
的字段属性b
- 可见性为
protected
的字段属性c
- 可见性为
public
的字段属性d
- 可见性为
public
的getter属性a
,将覆盖字段属性a
- 可见性为
private
的getter属性d
,将覆盖字段属性d
最终可用属性为public
的getter属性a
,public
的字段属性b
,
protected
的字段属性c
,private
的getter属性d
。
属性能否被生成类使用,还依赖于其可见性。可见性不是绝对的,而是相对的。就常理而言,属性在生成类中可见,才能被合法使用。
- 生成类和原类同一个包,只要不是
private
的属性就是可见的。对于上面的例子,a
,b
,c
都是可见的属性。 - 生成类和原类不同包,只有
public
的属性才是可见的。对于上面的例子,a
,b
是可见的。
Beanknife生成新类必须使用一个已有类作为模板。这个已有类就称为原类。而生成的新类就称为生成类。 Beanknife基于注解来配置生成类的具体细节。用于放置注解的类就是配置类。可以在原类上直接进行配置,这时原类同时也是配置类; 也可以在第三方类上进行配置,这时这个第三方类就是配置类。推荐使用后者,这样一没有侵入性,二支持扩展属性。
是否生成类,基于什么类来生成类,生成类的包名,类名,是由@ViewOf
注解唯一决定的。
虽然还存在大量其他注解可以用于配置生成类,但都必须遵循一个大前提,那就是已经配置了@ViewOf
注解。
@ViewOf
最重要且不可代替的三个属性是value
,config
和genPackage
。
value
决定了基于什么类来生成新类。即原类。若不指定,则默认使用注解所在的当前类。config
决定了配置类的位置。没错,别怀疑,@ViewOf
注解不一定要和配置类放一起。 当然若不指定,默认情况下@ViewOf
注解所在的类就是配置类。genPackage
决定了生成类所在的包。默认为原类所在的包。 这样的好处是基础属性的可见性会更高,可以使用尽可能多的基础属性。
其他属性参见配置注解。
除了基础属性,Beanknife还支持扩展属性。若要使用扩展属性,配置类不能等于原类,即必须使用第三方类作为配置类。理由看下去就能明白。
扩展属性按定义方式可分为
- 扩展字段属性: 在配置类中使用字段定义扩展属性,属性名由相关注解决定,字段类型即属性类型。
- 扩展方法属性: 在配置类中使用方法定义扩展属性,属性名由相关注解决定,方法返回类型型即属性类型。
扩展属性按作用方式可分为
- 覆盖: 使用
@OverrideViewProperty
注解,覆盖同名基础属性。要求对应基础属性必须存在且在生成类中可见。 - 映射: 使用
@MapViewProperty
注解,覆盖指定基础属性,并使用新的属性名。要求对应基础属性必须存在且在生成类中可见。 - 新增: 使用
@NewViewProperty
注解,增加一个新的属性,属性名不能与已经存在且可见的基础属性冲突。
对于扩展方法属性又可分为
- 静态: 方法上不存在
@Dynamic
注解。这里的静态意味着该属性在生成类中存在真实存在的对应字段,所定义的扩展方法仅仅用于为该字段赋初始值,不影响该字段后续读写操作。 - 动态: 方法被
@Dynamic
注释。这里的动态意味着该属性在生成类中不存在对应字段,扩展方法将在属性对应的getter方法中被执行,用以实时获得属性值。
下面通过几个例子来具体说明扩展属性的各种类型
@ViewOf(Bean.class)
class FieldDtoConfiguration {
@OverrideViewProperty("a")
@NullStringAsEmpty
private String a; // (1)
@MapViewProperty(name="newB", map="b")
private int b; // (2)
@NewViewProperty("f")
private String f; // (3)
}
-
字段a定义了一个覆盖扩展字段属性。它覆盖了原类中的基本属性a。 这里的覆盖也可以理解为修改。若想修改一个基本属性在生成类中的可见性和类型,就需要使用覆盖操作。 对于扩展字段属性,修改最终类型可以通过两个途径。
- 通过converter,本例中
@NullStringAsEmpty
相当于@UsePropertyConverter(NullStringAsEmptyConverter.class)
。 NullStringAsEmptyConverter是一个PropertyConverter, 具有convert方法,可用于将对象由String
转为String
。这里具体作用是若为null则转为空字符串,否则不变。 - 若指定的类型恰好是原类对应基础字段类型的生成类或生产类的同构体,并且转换方法不需要额外参数,则该转换能自动完成。
举个例子:若存在基础属性a,类型为A,并存在以A为原类的生成类
ADto
,则可以定义一个类型为ADto
的扩展字段属性覆盖a,两者间的类型转换将由工具自动完成。 更进一步,若a的类型为List<A>
,则对应的生成类同构体为List<ADto>
,若a的类型为Map<String, A[]>[]
,则对应的生成类同构体为Map<String, ADto[]>[]
。List
,Set
, 和Array
都是支持的。
- 通过converter,本例中
-
字段
b
覆盖隐藏了基础属性b
,并定义了一个新的映射扩展字段属性,属性名为newB
。映射扩展字段属性本质上只是覆盖扩展字段属性加改个名。所以覆盖扩展字段属性能做到的,它也能做到。 -
字段
f
定义了一个新增扩展字段属性。它在原类中没有对应的基础属性,所以在初始化时,需要额外为其传入一个初始值。
@ViewOf(Bean.class)
class MethodDtoConfiguration {
// (1)
@OverrideViewProperty("a")
public static String a(@InjectProperty("a") String a) {
return a != null ? a : "";
}
// (2)
@MapViewProperty(name="newB", map="b")
@Dynamic
public static String b(@InjectProperty("b") String a) {
return "new" + a;
}
// (3)
@NewViewProperty("now")
public static Date now() {
return new Date();
}
// (4)
@NewViewProperty("f")
@Dynamic
public static String f(@InjectProperty("newB") String newB, @InjectProperty("now") Date now) {
return newB + now;
}
}
-
定义了静态覆盖扩展方法属性
a
,实际效果和上例中定义的覆盖扩展字段属性a
一样,都是将基础属性a
在其为空的情况下转为空字符串。 这里的@InjectProperty
用于注入原类的属性。除了注入单个属性,还能注入整个原类实例String a(Bean source)
,或添加额外参数String a(Bean source, @ExtraParam("extraParam") String extraParam)
。 静态方法属性只会在初始化时起作用,所以只能接受原类或原类的属性作为参数。 对于静态方法属性,参数必须满足下来三种情况之一,顺序并不重要。- 参数类型为原类,用于注入原类实例,这种情况的参数有且只能有一个。
- 参数上有
@InjectProperty
注解,用于注入原类的某个可见的基础属性。可以有任意多个。 - 参数上有
@ExtraParam
注解,声明该参数是额外参数,在转换原类时必须额外传入。注意,一旦存在额外参数,上文覆盖字段属性中提到的原类到生成类的自动转换将不可用。
下面举几个例子
i. 对于
String a(@InjectProperty("a") String a)
,生成类中将生成如下代码public BeanView(Bean source) { this.a = MethodDtoConfiguration.a(source.getA()); // other fields initialization }
ii. 对于
String a(Bean source)
,生成类中将生成如下代码public BeanView(Bean source) { this.a = MethodDtoConfiguration.a(source); // other fields initialization }
iii. 对于
String a(Bean source, @ExtraParam("extraParam") String param)
,生成类中将生成如下代码public BeanView(Bean source, String extraParam) { this.a = MethodDtoConfiguration.a(source, extraParam); // other fields initialization }
-
定义了动态映射扩展方法属性
newB
,覆盖了基础属性b
。 不同于静态扩展方法属性,动态扩展方法属性在生成类中不存在对应的字段,它将在对应属性的getter方法中被实时地执行。 所以不同于静态扩展方法属性,动态扩展方法属性不支持注入原类实例,但可以注入生成类实例,也就是this
对象。另一方面额外参数也是不支持的,因为getter方法是无参的。 对于动态方法属性,参数必须满足下来两种情况之一,顺序并不重要。- 参数上有
@InjectSelf
注解,注入生成类实例,也就是this
对象。当然参数类型也必须正确。 - 参数上有
@InjectProperty
注解,注入生成类的指定属性,参数类型必须等于属性类型或是其基类。
下面举几个例子,假设属性名都是a
i. 对于
String a(@InjectProperty("a") String a)
,生成类中将生成如下代码public String getA() { return MethodDtoConfiguration.a(this.a); }
ii. 对于
String a(BeanView source)
,生成类中将生成如下代码public String getA() { return MethodDtoConfiguration.a(this); }
iii. 对于
String a(@InjectProperty("newB") String newB, @InjectProperty("now") Date now)
,生成类中将生成如下代码public String getA() { return MethodDtoConfiguration.a(this.getNewB(), this.now); }
- 参数上有
-
定义了静态新增扩展方法属性now,生成如下代码
public BeanView(Bean source) { // other fields initialization this.now = MethodDtoConfiguration.now(); // other fields initialization }
-
定义了动态新增扩展方法属性f,生成如下代码
public String getF() { return MethodDtoConfiguration.f(this.getNewB(), this.now); }
注意这里的
newB
属性是在2中定义的,now
属性是在3中定义的,都是原类说没有的。 对于newB
属性,因为它是动态方法属性,所以不存在对应字段,所以生成代码中使用了getter方法获取,而非字段获取。
Beanknife最大的目的就是为了偷懒,所以简化配置也是重点之一。于是配置继承就成了不可或缺的功能。
Beankinfe的配置类支持继承机制。这也是推荐使用配置类而不是直接在原类上配置的原因之一。
注解的继承性是Java的原生语言特性,Beanknife的配置继承就利用了这一特性。
所以判断一个注解是否支持继承,只要看它是否被@Inherited
所注解。
因为java只支持类级别的注解继承。所以也只有用于类上的配置注解可以被继承。
事实上除了@ViewOf
,几乎所有的类级别的配置注解都支持继承。而对于@ViewOf
,它的绝大多数属性,都有对应的独立注解存在。而这些注解都是支持继承的。
这意味着Beanknife几乎所有配置都支持继承。
Beanknfie并不存在显式的全局配置机制。但这一功能可以通过设置一个公共基类,而其他所有配置类都继承这个基类实现。
因为Beanknife的类级配置几乎都围绕@ViewOf
展开,所以这里列出@ViewOf
的各属性,对应的可继承独立注解,以及继承方式。
属性 | 独立注解 | 合并方式 | 子类上的值 | 基类上的值 | 最终值 |
---|---|---|---|---|---|
access |
ViewAccess |
override | Access.NONE |
Access.PUBLIC |
Access.NONE |
includes |
ViewPropertiesInclude |
union | {"a", "b"} |
{"b", "c"} |
{"b", "c", "a", "b"} |
excludes |
ViewPropertiesExclude |
union | {"a", "b"} |
{"b", "c"} |
{"b", "c", "a", "b"} |
includePattern |
ViewPropertiesIncludePattern |
append | "[aA]pple\\d" |
"[oO]range\\d" |
"[oO]range\\d [aA]pple\\d" |
excludePattern |
ViewPropertiesExcludePattern |
append | "[aA]pple\\d" |
"[oO]range\\d" |
"[oO]range\\d [aA]pple\\d" |
emptyConstructor |
ViewEmptyConstructor |
override | Access.NONE |
Access.PUBLIC |
Access.NONE |
fieldsConstructor |
ViewFieldsConstructor |
override | Access.NONE |
Access.PUBLIC |
Access.NONE |
copyConstructor |
ViewCopyConstructor |
override | Access.NONE |
Access.PUBLIC |
Access.NONE |
readConstructor |
ViewReadConstructor |
override | Access.NONE |
Access.PUBLIC |
Access.NONE |
getters |
ViewGetters |
override | Access.NONE |
Access.PUBLIC |
Access.NONE |
setters |
ViewSetters |
override | Access.NONE |
Access.PUBLIC |
Access.NONE |
errorMethods |
ViewErrorMethods |
override | false |
true |
false |
serializable |
ViewSerializable |
override | false |
true |
false |
serialVersionUID |
ViewSerialVersionUID |
override | 1L |
0L |
1L |
useDefaultBeanProvider |
ViewUseDefaultBeanProvider |
override | false |
true |
false |
configureBeanCacheType |
ViewConfigureBeanCacheType |
override | CacheType.NONE |
CacheType.LOCAL |
CacheType.NONE |
为了更好的说明配置继承机制,这里举几个例子
@ViewSerializable(true)
@ViewReadConstructor(Access.NONE)
@ViewSetters(Access.PROTECTED)
@ViewGenNameMapper("${name}Dto")
@ViewPropertiesExclude("a")
public class GrandparentViewConfigure {
}
@ViewGenNameMapper("ViewOf${name}")
@ViewPropertiesIncludePattern(".*")
public class Parent1ViewConfigure extends GrandparentViewConfigure {
}
@ViewOf(Leaf11Bean.class)
public class Leaf11BeanViewConfigure extends Parent1ViewConfigure {
}
@ViewOf(Leaf12Bean.class)
public class Leaf12BeanViewConfigure extends Parent1ViewConfigure {
}
@ViewSetters(Access.NONE)
// Not work, because "a" is excluded by parent configuration.
@ViewPropertiesInclude("a")
@ViewPropertiesExclude("b")
public class Parent2ViewConfigure extends GrandparentViewConfigure {
}
@ViewOf(Leaf21Bean.class)
@ViewPropertiesInclude(Leaf21BeanMeta.c)
public class Leaf21BeanViewConfigure extends Parent2ViewConfigure {
}
GrandparentViewConfigure
└───Parent1ViewConfigure
│ Leaf11BeanViewConfigure
│ Leaf12BeanViewConfigure
└───Parent2ViewConfigure
Leaf21BeanViewConfigure
Parent1ViewConfigure继承了GrandparentViewConfigure的配置,根据上表,得到了如下的等价配置
@ViewSerializable(true)
@ViewReadConstructor(Access.NONE)
@ViewSetters(Access.PROTECTED)
@ViewPropertiesExclude("a")
@ViewGenNameMapper("ViewOf${name}")
@ViewPropertiesIncludePattern(".*")
class Parent1ViewConfigure {
}
而Leaf11BeanViewConfigure和Leaf12BeanViewConfigure各自都继承了Parent1ViewConfigure。
不同于Parent1ViewConfigure和GrandparentViewConfigure,
Leaf11BeanViewConfigure和Leaf12BeanViewConfigure都使用了@ViewOf
注解,这意味着只有它们俩才会真正激活以上那些配置,生成新的类。
做个类比,Parent1ViewConfigure和GrandparentViewConfigure相当于java中的接口,并不真正起作用,
只有当,Leaf11BeanViewConfigure和Leaf12BeanViewConfigure则相当于java中的非抽象类,实现了接口。
Parent2ViewConfigure也继承了GrandparentViewConfigure的配置,根据上表,得到了如下的等价配置
@ViewSerializable(true)
@ViewReadConstructor(Access.NONE)
@ViewGenNameMapper("${name}Dto")
@ViewSetters(Access.NONE)
@ViewPropertiesInclude("a")
@ViewPropertiesExclude({"a", "b"})
public class Parent2ViewConfigure {
}
而Leaf21BeanViewConfigure继承了Parent2ViewConfigure并真正产生新代码。不同于上例,Leaf21BeanViewConfigure本身也带有配置并覆盖了上级配置,最终等价于
@ViewOf(Leaf21Bean.class)
@ViewSerializable(true)
@ViewReadConstructor(Access.NONE)
@ViewGenNameMapper("${name}Dto")
@ViewSetters(Access.NONE)
@ViewPropertiesExclude({"a", "b"})
@ViewPropertiesInclude({"a", Leaf21BeanMeta.c})
public class Leaf21BeanViewConfigure {
}
在定义映射或覆盖扩展字段属性时,若定义的字段类型与原类中目标字段的类型不同,则要么符合自动转换要求,要么使用属性转换器。 前者稍候会进行讨论,这里主要说明后者。
属性转换器是一个普通的非泛型,非抽象类,实现了PropertyConverter接口。然而PropertyConverter是泛型的,具有泛型参数From
和To
。
所以转换器实现该接口时需要通过泛型参数指定所支持的具体的转换类型。当前版本不支持转换泛型类型,未来可能支持。
以下是一个属性转换器的例子
public class NullStringAsEmptyConverter implements PropertyConverter<String, String> {
@Override
public String convert(String value) {
return value != null ? value : "";
}
@Override
public String convertBack(String value) {
return value;
}
}
NullStringAsEmptyConverter实现PropertyConverter时,把泛型From
和To
都设置为了String
,这代表这个属性转换器支持String
到String
的双向转换。
本例中,NullStringAsEmptyConverter会将值为null
的字段转换为空字符串,反向转换时则原样返回。这里的convert
指从原类字段转换到生成类的对应字段。,而convertBack
则正相反。
有多种激活属性转换器的方法。
- 使用注解UsePropertyConverter,像这样
@UsePropertyConverter(NullStringAsEmptyConverter.class)
. UsePropertyConverter
是可以注释注解的,同时也是可重复的,即同时存在多个UsePropertyConverter
实例。于是可以自建一个新的注解,然后使用UsePropertyConverter
注释自建的注解,让自建的注解获得UsePropertyConverter
等同的功能。 比如这样
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@UsePropertyConverter(NullBigDecimalAsZeroConverter.class)
@UsePropertyConverter(NullBigIntegerAsZeroConverter.class)
@UsePropertyConverter(NullByteAsZeroConverter.class)
@UsePropertyConverter(NullDoubleAsZeroConverter.class)
@UsePropertyConverter(NullFloatAsZeroConverter.class)
@UsePropertyConverter(NullIntegerAsZeroConverter.class)
@UsePropertyConverter(NullLongAsZeroConverter.class)
@UsePropertyConverter(NullShortAsZeroConverter.class)
public @interface NullNumberAsZero { }
被NullNumberAsZero所注释的字段,会自动从NullBigDecimalAsZeroConverter
,NullBigIntegerAsZeroConverter
,NullByteAsZeroConverter
,
NullDoubleAsZeroConverter
,NullFloatAsZeroConverter
,NullIntegerAsZeroConverter
,NullLongAsZeroConverter
和NullShortAsZeroConverter
中匹配最合适的转换器。即多个@UsePropertyConverter
之间是或的关系。当然不自建注解,直接将多个@UsePropertyConverter
直接用在目标字段上也是可以的。
扩展字段属性使用了属性转换器后将获得类似静态扩展方法属性类似的效果,属性转换器将在字段初始化阶段被使用,并不影响之后的getter
和setter
。
对于
@NullNumberAsZero
@OverrideViewProperty("a")
private int a;
假设原类a
字段是Integer
类型的,Beanknife会自动选择并使用NullIntegerAsZeroConverter,在生成类中产生如下代码
public static GeneratedBean read(OriginalBean source) {
GeneratedBean out = new GeneratedBean();
// other initialize statement.
out.a = new NullIntegerAsZeroConverter().convert(source.getA());
// other initialize statement.
return out;
}
Beanknife的一个重要的使用场景是基于原类,在保留尽可能多的信息的前提下,生成一个无循环引用版本的DTO。所以生成类的某个属性是另一个生成类,或生成类的集合的情况是非常常见的。 所以Beanknife对于某个属性的类型,从原类到生成类的转换提供了内置的自动机制。
注意
自动嵌套转换机制只在扩展字段属性上生效。因为扩展方法属性已经完全自定义了属性的生成。另一方面,属性转换器的优先级高于自动转换机制。即如果使用了属性转换器,则会禁用自动转换机制。
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
@Id
private String number;
private String name;
private String sex;
@ManyToOne
private Department department;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Department {
@Id
private String number;
@OneToMany(mappedBy = "department")
private List<Employee> employees;
}
上面是两个很常见的JPA实体,Employee具有多对一关系department
,同时另一边Department具有一对多关系employees
,它们共同构成了双向关系的两边。
显然他们之间存在循环引用,所以无法直接进行json序列化,一般需要通过配置,忽略掉特定的属性,来解除循环引用。
对于上例,有两个选择:
- 忽略
Employee.department
- 忽略
Department.employees
而Beanknife为用户带了了新的选择。我们可以选择全都要。Beanknfie可以很方便地通过生成一对没有循环引用版本的新类来规避循环引用,更重要的是Beanknife并不限制生成类的数量。 基于同一个类,可以生成任意多个生成类,只要生成类的全限定名各不相同就行。
-
从
Department
的角度来看,Department.employees.department
永远指向自身,所以可以毫无顾虑地除去。@ViewPropertiesIncludePattern(".*") class BaseConfiguration {}
基类配置,包含所有原类可见属性
@ViewOf(value = Employee.class, genName = "EmployeeInfo") @RemoveViewProperty("department") class EmployeeInfoConfiguration extends BaseConfiguration {}
生成
EmployeeInfo
,相比原类,去除了department
属性@ViewOf(value = Department.class, genName = "DepartmentDetail") class DepartmentDetailConfiguration extends BaseConfiguration { @OverrideViewProperty("employees") private List<EmployeeInfo> employees; }
生成
DepartmentDetail
,相比原类,employees
的类型从List<Employee>
变为了List<EmployeeInfo>
。 此处正是使用了自动嵌套转换机制。类型的转换被工具自动支持,不需要另写属性转换器。 -
从
Employee
的角度看来,从Employee.department.employees
开始,同样会造成上面提到的循环引用。 常规方案是去除Department.employees
属性,但假设客户端还希望同时查询雇员的同事,即department.employees
。 为了保留Department.employees
,我们可以直接使用上面的生成类DepartmentDetail
。于是有如下配置:@ViewOf(value = Employee.class, genName = "EmployeeDetail") class EmployeeDetailConfiguration extends BaseConfiguration { @OverrideViewProperty("department") private DepartmentDetail department; }
多亏自动嵌套转换机制,我们将
Employee.department
属性的类型从Department
换成了它的生成类DepartmentDetail
。EmployeeDetail.department.employees
不再会造成循环引用。在这个例子中,我们对多级使用了生成类自动转换,嵌套因此而得名。
施工中...
施工中...
施工中...
施工中...
施工中...
施工中...