Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow users to skip register class for application/json #36

Merged
merged 14 commits into from
Dec 5, 2024
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,105 @@ public class FuryResources {
}
```

## Use Apache Fury with Quarkus REST/RESTEasy
You can send a http request with Fury protocol, and let Fury to handle your objects serialization.

The usage will be different if class registration is disabled or enabled:
- Enable class registration: you must register class with same ID as the server, you should assign an id using
`@FurySerialization(classId = xxx)`, otherwise Fury will allocate an auto-generated ID which you won't know at the
client for registration.
- Disable class registration: no class id are needed to register, which is more easy to use, but the serialized size
will be larger since Fury will serialize class as a string instead of an id. Note that `quarkus-fury` will only allow
classes annotated by `@FurySerialization` for deserialization, the deserialization will be safe as class registration
enabled.

### Class registration enabled
Server example:
```java
import io.quarkiverse.fury.FurySerialization;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@FurySerialization(classId = 100)
record Struct(int f1, String f2) {
}

@Path("/fury")
@ApplicationScoped
public class FuryResources {
@POST
@Path("/struct")
@Produces("application/fury")
@Consumes("application/fury")
public Struct testStruct(Struct obj) {
return new Struct(10, "abc");
}
}
```

Client example:
```java
import static io.restassured.RestAssured.given;
import org.apache.fury.ThreadSafeFury;
import io.restassured.RestAssured;
import io.restassured.response.Response;

public class FuryClient {
private static ThreadSafeFury fury = Fury.builder().requireClassRegistration(false).buildThreadSafeFury();
static {
fury.register(Struct.class, 100, true);
}

public static void main(String[] args) {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
Struct struct = Struct.create();
Response response = given().contentType("application/fury").body(fury.serialize(struct)).when()
.post("/fury/struct").then().statusCode(200).contentType("application/fury").extract().response();
byte[] result = response.body().asByteArray();
Struct struct1 = (Struct) fury.deserialize(result);
System.out.println(struct1);
}
}
```

### Class registration disabled
Server example:
```java
@FurySerialization
record Struct(int f1, String f2) {
}

@Path("/fury")
@ApplicationScoped
public class FuryResources {
@POST
@Path("/struct")
@Produces("application/fury")
@Consumes("application/fury")
public Struct testStruct(Struct obj) {
return new Struct(10, "abc");
}
}
```

Client example
```java
public class RestClient {
private static ThreadSafeFury fury = Fury.builder().requireClassRegistration(false).buildThreadSafeFury();

public static void main(String[] args) {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
Struct struct = Struct.create();
Response response = given().contentType("application/fury").body(fury.serialize(struct)).when()
.post("/fury/struct").then().statusCode(200).contentType("application/fury").extract().response();
byte[] result = response.body().asByteArray();
Struct struct1 = (Struct) fury.deserialize(result);
System.out.println(struct1);
}
}
```

More details about usage can be found [here](https://docs.quarkiverse.io/quarkus-fury/dev/index.html).

## Contributors ✨
Expand Down
3 changes: 2 additions & 1 deletion docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ public class ExampleResource {
}
----

NOTE: The class ID (or serialization identifier) must be identical on both the client and server sides. A mismatch in the class ID will result in serialization or deserialization issues, potentially causing runtime errors or data corruption.
NOTE: If class registration is enabled, the class ID (or serialization identifier) must be identical on both the client and server sides. A mismatch in the class ID will result in serialization or deserialization issues, potentially causing runtime errors or data corruption.
If class registration is disabled, please do not assign class id at client and server slides.

[[extension-configuration-reference]]
== Extension Configuration Reference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ public Boolean testSerializePOJO() {
return struct1.equals(struct2);
}

@POST
@Path("/struct")
@Produces("application/fury")
@Consumes("application/fury")
public Struct testStruct(Struct obj) {
Preconditions.checkArgument(obj.equals(Struct.create()), obj);
return Struct.create();
}

@POST
@Path("/test")
@Produces("application/fury")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
quarkus.rest-client."io.quarkiverse.fury.it.Client".url=${test.url}

quarkus.fury.required-class-registration=false
quarkus.fury.register-class-names=io.quarkiverse.fury.it.Struct
quarkus.fury.register-class."io.quarkiverse.fury.it.Foo".class-id=300
quarkus.fury.register-class."io.quarkiverse.fury.it.Foo".serializer=io.quarkiverse.fury.it.FooSerializer
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static io.quarkiverse.fury.it.FuryResources.BAR_CLASS_ID;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.apache.fury.Fury;
import org.junit.jupiter.api.Assertions;
Expand All @@ -29,6 +28,18 @@ public void testThirdPartyBar() {
given().when().get("/fury/third_party_bar").then().statusCode(200).body(is("true"));
}

@Test
public void testFuryStruct() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
Struct struct = Struct.create();
Fury fury = Fury.builder().requireClassRegistration(false).withName("Fury" + System.nanoTime()).build();
Response response = given().contentType("application/fury").body(fury.serialize(struct)).when()
.post("/fury/struct").then().statusCode(200).contentType("application/fury").extract().response();
byte[] result = response.body().asByteArray();
Struct struct1 = (Struct) fury.deserialize(result);
Assertions.assertEquals(struct1, struct);
}

@Test
public void testFuryBar() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
Expand Down
64 changes: 45 additions & 19 deletions runtime/src/main/java/io/quarkiverse/fury/FuryRecorder.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package io.quarkiverse.fury;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.Function;

import org.apache.fury.BaseFury;
import org.apache.fury.Fury;
import org.apache.fury.ThreadLocalFury;
import org.apache.fury.ThreadSafeFury;
import org.apache.fury.config.Config;
import org.apache.fury.config.FuryBuilder;
import org.apache.fury.resolver.ClassChecker;
import org.apache.fury.resolver.ClassResolver;
import org.apache.fury.serializer.Serializer;
import org.apache.fury.util.GraalvmSupport;
import org.apache.fury.util.Preconditions;
import org.jboss.logging.Logger;

Expand All @@ -18,6 +26,9 @@
@Recorder
public class FuryRecorder {
private static final Logger LOG = Logger.getLogger(FuryRecorder.class);
private static final ConcurrentSkipListSet<String> annotatedClasses = new ConcurrentSkipListSet<>();
private static final ClassChecker checker = (classResolver, className) -> !GraalvmSupport.isGraalRuntime()
|| annotatedClasses.contains(className);

public RuntimeValue<BaseFury> createFury(
final FuryBuildTimeConfig config, final BeanContainer beanContainer) {
Expand All @@ -32,8 +43,13 @@ public RuntimeValue<BaseFury> createFury(
.deserializeNonexistentEnumValueAsNull(config.deserializeNonexistentEnumValueAsNull())
.withNumberCompressed(config.compressNumber())
.withStringCompressed(config.compressString());
BaseFury fury = config.threadSafe() ? builder.buildThreadSafeFury() : builder.build();

Function<ClassLoader, Fury> furyFactory = c -> {
Fury f = builder.withClassLoader(c).build();
f.getClassResolver().setClassChecker(checker);
return f;
};
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
BaseFury fury = config.threadSafe() ? new ThreadLocalFury(furyFactory) : furyFactory.apply(classLoader);
// register to the container
beanContainer.beanInstance(FuryProducer.class).setFury(fury);
return new RuntimeValue<>(fury);
Expand All @@ -49,6 +65,7 @@ public void registerClassByName(final RuntimeValue<BaseFury> furyValue, final St
.loadClass(serializerClassName.get());
}
registerClass(furyValue, clazz, classId, serializer);
annotatedClasses.add(className);
} catch (ClassNotFoundException e) {
LOG.warn("can not find register class: " + className
+ (serializerClassName.isPresent()
Expand All @@ -61,29 +78,17 @@ public void registerClassByName(final RuntimeValue<BaseFury> furyValue, final St
public void registerClass(
final RuntimeValue<BaseFury> furyValue, final Class<?> clazz,
final int classId, Class<? extends Serializer> serializer) {
annotatedClasses.add(clazz.getName());
BaseFury fury = furyValue.getValue();
ClassResolver classResolver = getClassResolver(fury);
Config config = classResolver.getFury().getConfig();
if (classId > 0) {
Preconditions.checkArgument(
classId >= 256 && classId <= Short.MAX_VALUE,
"Class id %s must be >= 256 and <= %s",
classId,
Short.MAX_VALUE);
Class<?> registeredClass;
if (fury instanceof ThreadSafeFury) {
ThreadSafeFury threadSafeFury = (ThreadSafeFury) fury;
registeredClass = (threadSafeFury).execute(f -> f.getClassResolver().getRegisteredClass((short) classId));
if (serializer == null) {
// Generate serializer bytecode.
threadSafeFury.execute(f -> f.getClassResolver().getSerializerClass(clazz));
}
} else {
ClassResolver classResolver = ((Fury) fury).getClassResolver();
registeredClass = classResolver.getRegisteredClass((short) classId);
if (serializer == null) {
// Generate serializer bytecode.
classResolver.getSerializerClass(clazz);
}
}
Class<?> registeredClass = classResolver.getRegisteredClass((short) classId);
Preconditions.checkArgument(
registeredClass == null,
"ClassId %s has been registered for class %s",
Expand All @@ -92,10 +97,31 @@ public void registerClass(
fury.register(clazz, (short) classId);
} else {
// Generate serializer bytecode.
fury.register(clazz, serializer == null);
if (config.requireClassRegistration()) {
fury.register(clazz, serializer == null);
}
}
if (serializer != null) {
fury.registerSerializer(clazz, serializer);
} else {
// Generate serializer bytecode.
try {
Method createSerializerAhead = ClassResolver.class.getDeclaredMethod(
"createSerializerAhead", Class.class);
createSerializerAhead.setAccessible(true);
zhfeng marked this conversation as resolved.
Show resolved Hide resolved
createSerializerAhead.invoke(classResolver, clazz);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}

private ClassResolver getClassResolver(BaseFury fury) {
if (fury instanceof ThreadSafeFury) {
ThreadSafeFury threadSafeFury = (ThreadSafeFury) fury;
return threadSafeFury.execute(Fury::getClassResolver);
} else {
return ((Fury) fury).getClassResolver();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.apache.fury.ThreadSafeFury;
import org.apache.fury.io.FuryInputStream;
import org.apache.fury.resolver.ClassResolver;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
Expand Down Expand Up @@ -64,6 +66,11 @@ protected boolean isSupportedMediaType(MediaType mediaType) {
}

protected boolean canSerialize(final Class<?> aClass) {
Config config = ConfigProvider.getConfig();
Boolean requiredClassRegistration = config.getValue("quarkus.fury.required-class-registration", Boolean.class);
if (!requiredClassRegistration) {
return true;
}
if (getFury() instanceof final ThreadSafeFury threadSafeFury) {
return (threadSafeFury).execute(f -> f.getClassResolver().getRegisteredClassId(aClass)) != null;
} else {
Expand Down
1 change: 1 addition & 0 deletions runtime/src/main/resources/application.properties
chaokunyang marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
quarkus.http.port=8087
Loading