jaxb-api
jaxb-api copied to clipboard
Support for `record`
With the release of Java 16 in March 2021 the broad use of the record keyword is expected to grow.
I'd like to propose that the JAXB API clearly defines if and how the record keyword is supported by JAXB:
- Can records be used with JAXB?
- How to annotate records, as most methods and fields no exists unless overwritten?
- Are there benefits / drawbacks of using records over custom classes in direct relation to JAXB?
- Is support for records mandatory for implementations of the JAXB API (hence, is it safe to be used by vendor-agnositic applications)?
The Java 17 LTS release further pushes people towards records. Have you ever heard anything about this topic from anyone, @mkarg ?
@winne42 I am not aware of any information besides what you can read on this page.
Thanks @mkarg , sad...
@winne42 feel free to propose a solution you want to see
some news on this?
I expected this to work.
@XmlRootElement(namespace = "example.books")
public record Bookstore(
String name,
String location,
@XmlElementWrapper(name = "bookList")
@XmlElement(name = "book") List<Book>
bookList
) {
}
@XmlRootElement(name = "book")
@XmlType(propOrder = { "author", "name", "publisher", "isbn" })
public record Book(
@XmlElement(name = "title")
String name,
String author,
String publisher,
String isbn) {
}
Here are some more of my thoughts on this topic:
Instantiating Classes vs Records
The main difference to the current implementations is the processing order. Classes/beans could be creates using the default no args Constructor and then setting the values using fields or methods. Records must be creates using the Constructor that describes every single attribute. So you need to hold all needed elements for an record in cache until you can construct the record with these elements.
Java 16++
Records are a Feature of Java 16 you have to find a way to handle language features. Jaxb-api and implementations will not depend on Java 17. Options would be Multi-Release or Reflections.
This POC shows a way to read and instantiate Records with Java 11
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Stream;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
public class Main {
public static void main(String[] args) throws JAXBException {
Map<String, Object> m = new HashMap<>();
m.put("name", "theName");
m.put("author", "theAuther");
m.put("publisher", "thePublisher");
m.put("isbn", "theIsbn");
double javaVersion = RecordUtil.javaVersion();
System.out.println("javaVersion: " + javaVersion);
boolean isRecord = RecordUtil.isRecord(Book_minimal.class);
System.out.println("isRecord: " + isRecord);
Object book = RecordUtil.instanceOfMap(Book_minimal.class, m);
System.out.println(book);
Method[] methodsOfRecord = RecordUtil.methodsOfRecord(Book_minimal.class);
System.out.println("methodsOfRecord:");
for (Method method : methodsOfRecord) {
System.out.println("- " + method);
try {
Object returnVal = method.invoke(book);
System.out.println("= " + returnVal);
} catch (Exception e) {
e.printStackTrace();
}
}
JAXBContext jc = JAXBContext.newInstance(Book_minimal.class);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(book, System.out);
}
// Handles actions on Record with Java 11
static class RecordUtil {
private static Method class_getRecordComponentsMethod = null;
private static Method recordComponent_getAccessorMethod = null;
static double javaVersion() {
return Double.parseDouble(System.getProperty("java.class.version"));
}
static boolean isRecord(Class<?> cls) {
if (javaVersion() < 60) {
// TODO: 61 for java17
return false;
}
try {
Method m = Class.class.getMethod("isRecord");
if (m == null) {
return false;
}
Boolean b = (Boolean) m.invoke(cls);
return b.booleanValue();
} catch (Exception e) {
return false;
}
}
static Method[] methodsOfRecord(Class<?> recordClass) {
if (recordComponent_getAccessorMethod == null) {
try {
final Class<?> recordComponentClass = Class.forName("java.lang.reflect.RecordComponent");
recordComponent_getAccessorMethod = recordComponentClass.getMethod("getAccessor");
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
Object[] recordComponents = recordComponents(recordClass);
Method[] methods = Stream.of(recordComponents).map(rc -> {
try {
return recordComponent_getAccessorMethod.invoke(rc);
} catch (Exception e) {
throw new IllegalArgumentException("Could not get Method-Accessor of the RecordComponent: " + rc,
e);
}
}).toArray(Method[]::new);
return methods;
}
static Object[] recordComponents(Class<?> recordClass) {
try {
if (class_getRecordComponentsMethod == null) {
class_getRecordComponentsMethod = Class.class.getMethod("getRecordComponents");
}
return (Object[]) class_getRecordComponentsMethod.invoke(recordClass);
} catch (Exception e) {
throw new IllegalArgumentException("", e);
}
}
static Object instanceOfMap(Class<Book_minimal> targetClass, Map<String, Object> m) {
Constructor<?> constr = Book_minimal.class.getDeclaredConstructors()[0];
int count = constr.getParameterCount();
Object[] objects = new Object[count];
Parameter[] params = constr.getParameters();
for (int i = 0; i < count; i++) {
Parameter param = params[i];
System.out.println(param);
Object o = null;
Class<?> type = param.getType();
String name = param.getName();
if (m.containsKey(name)) {
o = m.get(name);
} else {
o = m.entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(name)).findFirst()
.map(Entry::getValue).orElse(null);
// spec multiple ignorecase take first;
}
// trypecheck
objects[i] = o;
}
try {
return constr.newInstance(objects);
} catch (Exception exception) {
throw new RuntimeException("Could not create the record: " + targetClass, exception);
}
}
}
}
Jaxb Features
@XmlElement - name, defaultValue, required, nillable, type
The @XmlElement features must work like with class
@XmlRootElement(name = "book")
public record Book_element(
@XmlElement(name = "title")
String name, //must work like class
@XmlElement(defaultValue = "me")
String author,//must work like class
@XmlElement(required = true, nillable = false)
String publisher,//must work like class
@XmlElement(type = String.class)
Object isbn //must work like class
) {
}
@XmlElement - factoryMethod, factoryClass
factoryClass and factoryMethod could not be used with Records because there is no option to set values after creating an Record and no way to provide arguments into the factory method because it must be a no-arg factory method.
@XmlType(factoryClass = Book4.class, factoryMethod = "newInstance")
public record Book4(String title, String author, String publisher, String isbn) {
public static Book4 newInstance() {
return new Book4("", "", "", "");
}
}
@XmlElementWrapper and @XmlJavaTypeAdapter
there are no implications that avoid the handling of@XmlElementWrapper or @XmlJavaTypeAdapter
@XmlRootElement(namespace = "example.books")
public record Bookstore_wrapper(
String name,
String location,
@XmlElementWrapper(name = "bookList")
@XmlElement(name = "book") List<Book_minimal>
bookList
) {
}
@XmlAccessorType
Record do not have an any setters and fields that are setable. Just arguments of an constructor and method to get access to the value of the RecordComponent. More documentation is needed.
what are the steps that must be done to bring this forward?
what are the steps that must be done to bring this forward?
Basing on the great research results already posted above, identify the items to actually get changed in the API, JavaDoc, and TCK. Provide a PR implementing your propsal. Discuss the PR with the committers. If a majority is convinced to implement the needed changes in their products, the PR will pass. :-)
Note that the EF actually likes to have a working product first before fixing the API, so it might sense to get one of the vendors into your boat, e. g. by adding a PR to a JAXB implementation, proving that your ideas will work in an actual product. :-)
FYI:
I did a POC that could marshal and unmarshal a Record to see how much work it would be in eclipse-ee4j/jaxb-ri
I am pretty sure someone else from the jaxb-ri team could do it much better! But maybe this helps others with the analysis.
any progrss?