Hamcrest actually follows the JavaBeans standard (which allows for arbitrary accessor names), so we can do this with hasProperty
. If you want to. I'm not sure you do, though - it's quite a hassle.
If we follow the workings of the source for HasPropertyWithValue
we find that it discovers the accessor method's name by finding the PropertyDescriptor
for the property in the BeanInfo
of the class concerned, retrieved by means of the java.beans.Introspector
.
The Introspector
has some very helpful documentation on how BeanInfo
for a given class is resolved:
The Introspector class provides a standard way for tools to learn
about the properties, events, and methods supported by a target Java
Bean.
For each of those three kinds of information, the Introspector will
separately analyze the bean's class and superclasses looking for
either explicit or implicit information and use that information to
build a BeanInfo object that comprehensively describes the target
bean.
For each class "Foo", explicit information may be available if there
exists a corresponding "FooBeanInfo" class that provides a non-null
value when queried for the information. We first look for the BeanInfo
class by taking the full package-qualified name of the target bean
class and appending "BeanInfo" to form a new class name. If this
fails, then we take the final classname component of this name, and
look for that class in each of the packages specified in the BeanInfo
package search path.
Thus for a class such as "sun.xyz.OurButton" we would first look for a
BeanInfo class called "sun.xyz.OurButtonBeanInfo" and if that failed
we'd look in each package in the BeanInfo search path for an
OurButtonBeanInfo class. With the default search path, this would mean
looking for "sun.beans.infos.OurButtonBeanInfo".
If a class provides explicit BeanInfo about itself then we add that to
the BeanInfo information we obtained from analyzing any derived
classes, but we regard the explicit information as being definitive
for the current class and its base classes, and do not proceed any
further up the superclass chain.
If we don't find explicit BeanInfo on a class, we use low-level
reflection to study the methods of the class and apply standard design
patterns to identify property accessors, event sources, or public
methods. We then proceed to analyze the class's superclass and add in
the information from it (and possibly on up the superclass chain).
You'd think the Introspector
could grok records and generate correct BeanInfo
in that last step (where "we use low-level reflection"), but it appears not to. If you google for a bit you'll find some talk on the JDK dev list about adding this, but nothing seems to have happened. Might be that the JavaBeans spec has to be updated, which I imagine could take some time.
But, to answer your question, all we have to do is provide a BeanInfo
for each and every record type you have. Handwriting them, however, isn't something we want to do - it's even worse than the old-fashioned way of writing classes with getters and setters (and equals
and hashCode
and whatnot).
We could autogenerate the bean info as a build step (or dynamically when we start the app). A somewhat simpler approach (which requires a bit of boilerplate) is making a generic BeanInfo
that can be used for all record classes. Here's a minimum-effort approach. First, suppose we have this record:
public record Point(int x, int y){}
And a main class that treats it as a 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);
}
}
}
If we just compile and run like java Main x y z
you get output like this:
Prop x: (No property with that name)
Prop y: (No property with that name)
Prop z: (No property with that name)
So it doesn't find the record components, as expected. Let's make a generic 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();
}
}
Having this class in our toolbox, all we have to do is extend it with a class of the right name. For our example, PointBeanInfo
in the same package as the Point
record:
public class PointBeanInfo extends RecordBeanInfo {
public PointBeanInfo() throws IntrospectionException {
super(Point.class);
}
}
With all these things in place, we run our main class and get the expected output:
$ java Main x y z
Prop x: 4
Prop y: 2
Prop z: (No property with that name)
Closing note: If you just want to use properties to make your unit tests look nicer, I suggest using one of the workarounds given in other answers, rather than the overengineered approach I present.
record
viaClass::isRecord
– Quinquennial