Hamcrest 实际上遵循 JavaBeans 标准(允许任意访问器名称),因此我们可以使用hasProperty
. 如果你想。不过,我不确定你是否这样做 - 这很麻烦。
如果我们遵循源代码HasPropertyWithValue
的工作原理,我们会发现它通过在相关类PropertyDescriptor
的 中查找属性来发现访问器方法的名称,并通过.BeanInfo
java.beans.Introspector
有一些关于如何解决给定类的非常Introspector
有用的文档:BeanInfo
Introspector 类为工具提供了一种了解目标 Java Bean 支持的属性、事件和方法的标准方法。
对于这三种信息中的每一种,Introspector 将分别分析 bean 的类和超类以查找显式或隐式信息,并使用该信息构建一个全面描述目标 bean 的 BeanInfo 对象。
对于每个类“Foo”,如果存在相应的“FooBeanInfo”类,该类在查询信息时提供非空值,则可以使用显式信息。我们首先通过获取目标 bean 类的完整包限定名称并附加“BeanInfo”以形成新的类名来查找 BeanInfo 类。如果这失败了,那么我们取这个名称的最终类名组件,并在 BeanInfo 包搜索路径中指定的每个包中查找该类。
因此,对于诸如“sun.xyz.OurButton”之类的类,我们将首先查找名为“sun.xyz.OurButtonBeanInfo”的 BeanInfo 类,如果失败,我们将在 BeanInfo 搜索路径中的每个包中查找 OurButtonBeanInfo 类。使用默认搜索路径,这意味着查找“sun.beans.infos.OurButtonBeanInfo”。
如果一个类提供了关于它自己的显式 BeanInfo,那么我们将它添加到我们通过分析任何派生类获得的 BeanInfo 信息中,但我们认为显式信息对于当前类及其基类是确定的,并且不再继续往下超类链。
如果我们没有在类上找到显式的 BeanInfo,我们会使用低级反射来研究类的方法并应用标准设计模式来识别属性访问器、事件源或公共方法。然后我们继续分析类的超类并添加来自它的信息(并且可能在超类链上)。
您会认为可以在最后一步(“我们使用低级反射”)中Introspector
挖掘记录并生成正确的记录,但似乎并非如此。BeanInfo
如果你谷歌一下,你会在 JDK 开发列表上找到一些关于添加它的讨论,但似乎什么也没发生。可能是 JavaBeans 规范必须更新,我想这可能需要一些时间。
但是,要回答您的问题,我们所要做的就是为BeanInfo
您拥有的每种记录类型提供一个。equals
然而,手写它们并不是我们想要做的事情——它甚至比使用 getter和setter(等等)编写类的老式方式更糟糕hashCode
。
我们可以自动生成 bean 信息作为构建步骤(或者在我们启动应用程序时动态生成)。一种更简单的方法(需要一些样板文件)是制作BeanInfo
可用于所有记录类的泛型。这是一种最小努力的方法。首先,假设我们有这个记录:
public record Point(int x, int y){}
以及一个将其视为 bean 的主类:
public class Main {
public static void main(String[] args) throws Exception {
var bi = java.beans.Introspector.getBeanInfo(Point.class);
var bean = new Point(4, 2);
for (var prop : args) {
Object value = Stream.of(bi.getPropertyDescriptors())
.filter(pd -> pd.getName().equals(prop))
.findAny()
.map(pd -> {
try {
return pd.getReadMethod().invoke(bean);
} catch (ReflectiveOperationException e) {
return "Error: " + e;
}
})
.orElse("(No property with that name)");
System.out.printf("Prop %s: %s%n", prop, value);
}
}
}
如果我们像java Main x y z
你一样编译和运行,得到如下输出:
Prop x: (No property with that name)
Prop y: (No property with that name)
Prop z: (No property with that name)
所以它没有像预期的那样找到记录组件。让我们做一个通用的BeanInfo
:
public abstract class RecordBeanInfo extends java.beans.SimpleBeanInfo {
private final PropertyDescriptor[] propertyDescriptors;
public RecordBeanInfo(Class<?> recordClass) throws IntrospectionException {
if (!recordClass.isRecord())
throw new IllegalArgumentException("Not a record: " + recordClass);
var components = recordClass.getRecordComponents();
propertyDescriptors = new PropertyDescriptor[components.length];
for (var i = 0; i < components.length; i++) {
var c = components[i];
propertyDescriptors[i] = new PropertyDescriptor(c.getName(), c.getAccessor(), null);
}
}
@Override
public PropertyDescriptor[] getPropertyDescriptors() {
return this.propertyDescriptors.clone();
}
}
在我们的工具箱中有这个类,我们所要做的就是用一个正确名称的类来扩展它。对于我们的示例,PointBeanInfo
在与记录相同的包中Point
:
public class PointBeanInfo extends RecordBeanInfo {
public PointBeanInfo() throws IntrospectionException {
super(Point.class);
}
}
有了所有这些东西,我们运行我们的主类并获得预期的输出:
$ java Main x y z
Prop x: 4
Prop y: 2
Prop z: (No property with that name)
结束语:如果您只想使用属性使单元测试看起来更好,我建议使用其他答案中给出的解决方法之一,而不是我提出的过度设计的方法。