Skip to content

Commit

Permalink
Hibernate reactive panache refactoring
Browse files Browse the repository at this point in the history
- do not store the current reactive session in the CDI request context
but instead in the vertx duplicated context
- do not offload execution of a panache entity method on the current
vertx context but instead validate that the method is executed on the
vedtx duplicated context
- introduce ReactiveSessionInterceptor
- ReactiveTransactionalInterceptor can only be used for method that
return Uni; this is validated at build time
- if resteasy-reactive is present then automatically associate the ReactiveSessionInterceptor with resource methods on classes that use a panache entity
- also remove the quarkus-integration-test-hibernate-reactive-panache-blocking module
  • Loading branch information
mkouba committed Dec 19, 2022
1 parent fc20fb2 commit 50a181f
Show file tree
Hide file tree
Showing 24 changed files with 801 additions and 791 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;

import org.hibernate.reactive.common.spi.Implementor;
import org.hibernate.reactive.common.spi.MutinyImplementor;
import org.hibernate.reactive.mutiny.Mutiny;
import org.hibernate.reactive.mutiny.impl.MutinySessionFactoryImpl;
Expand All @@ -28,7 +29,7 @@ public class ReactiveSessionFactoryProducer {
@ApplicationScoped
@DefaultBean
@Unremovable
@Typed({ Mutiny.SessionFactory.class, MutinyImplementor.class })
@Typed({ Mutiny.SessionFactory.class, MutinyImplementor.class, Implementor.class })
public MutinySessionFactoryImpl mutinySessionFactory() {
if (jpaConfig.getDeactivatedPersistenceUnitNames()
.contains(HibernateReactive.DEFAULT_REACTIVE_PERSISTENCE_UNIT_NAME)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.quarkus.hibernate.reactive.panache.common.deployment;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.Priority;
Expand All @@ -12,14 +15,24 @@
import javax.persistence.NamedQuery;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.IsTest;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -31,9 +44,13 @@
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.hibernate.orm.deployment.HibernateOrmEnabled;
import io.quarkus.hibernate.orm.deployment.JpaModelBuildItem;
import io.quarkus.hibernate.reactive.panache.common.ReactiveSession;
import io.quarkus.hibernate.reactive.panache.common.runtime.PanacheHibernateRecorder;
import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveSessionInterceptor;
import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional;
import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptor;
import io.quarkus.hibernate.reactive.panache.common.runtime.TestReactiveTransactionalInterceptor;
import io.smallrye.mutiny.Uni;

@BuildSteps(onlyIf = HibernateOrmEnabled.class)
public final class PanacheJpaCommonResourceProcessor {
Expand All @@ -42,6 +59,17 @@ public final class PanacheJpaCommonResourceProcessor {
private static final DotName DOTNAME_NAMED_QUERIES = DotName.createSimple(NamedQueries.class.getName());
private static final String TEST_REACTIVE_TRANSACTION = "io.quarkus.test.TestReactiveTransaction";

private static final DotName REACTIVE_TRANSACTIONAL = DotName.createSimple(ReactiveTransactional.class.getName());
private static final DotName REACTIVE_SESSION = DotName.createSimple(ReactiveSession.class.getName());
private static final DotName UNI = DotName.createSimple(Uni.class.getName());
private static final DotName PANACHE_ENTITY_BASE = DotName
.createSimple("io.quarkus.hibernate.reactive.panache.PanacheEntityBase");
private static final DotName PANACHE_ENTITY = DotName.createSimple("io.quarkus.hibernate.reactive.panache.PanacheEntity");
private static final DotName PANACHE_KOTLIN_ENTITY_BASE = DotName
.createSimple("io.quarkus.hibernate.reactive.panache.kotlin.PanacheEntityBase");
private static final DotName PANACHE_KOTLIN_ENTITY = DotName
.createSimple("io.quarkus.hibernate.reactive.panache.kotlin.PanacheEntity");

@BuildStep(onlyIf = IsTest.class)
void testTx(BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildProducer,
BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
Expand All @@ -61,12 +89,97 @@ void testTx(BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildPro
}

@BuildStep
void registerInterceptor(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
void registerInterceptors(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder();
builder.addBeanClass(ReactiveSessionInterceptor.class);
builder.addBeanClass(ReactiveTransactionalInterceptor.class);
additionalBeans.produce(builder.build());
}

@BuildStep
void validateInterceptedMethods(ValidationPhaseBuildItem validationPhase,
BuildProducer<ValidationErrorBuildItem> errors) {
for (BeanInfo bean : validationPhase.getContext().beans().withAroundInvokeInterceptor()) {
for (Entry<MethodInfo, Set<AnnotationInstance>> e : bean.getInterceptedMethodsBindings().entrySet()) {
if (e.getKey().returnType().name().equals(UNI)) {
// Method returns Uni - no need to iterate over the bindings
continue;
}
if (Annotations.contains(e.getValue(), REACTIVE_TRANSACTIONAL)) {
errors.produce(new ValidationErrorBuildItem(
new IllegalStateException("A method annotated with @ReactiveTransactional does not return Uni: "
+ e.getKey())));
} else if (Annotations.contains(e.getValue(), REACTIVE_SESSION)) {
errors.produce(new ValidationErrorBuildItem(
new IllegalStateException("A method annotated with @ReactiveSession does not return Uni: "
+ e.getKey())));
}
}
}
}

@BuildStep
void transformResourceMethods(CombinedIndexBuildItem index, Capabilities capabilities,
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer) {
if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) {
// Custom request method designators are not supported
Set<DotName> designators = Set.of(DotName.createSimple("javax.ws.rs.GET"), DotName.createSimple("javax.ws.rs.HEAD"),
DotName.createSimple("javax.ws.rs.DELETE"), DotName.createSimple("javax.ws.rs.OPTIONS"),
DotName.createSimple("javax.ws.rs.PATCH"), DotName.createSimple("javax.ws.rs.POST"),
DotName.createSimple("javax.ws.rs.PUT"));

// Collect all panache entities
Set<DotName> entities = new HashSet<>();
for (ClassInfo subclass : index.getIndex().getAllKnownSubclasses(PANACHE_ENTITY_BASE)) {
if (!subclass.name().equals(PANACHE_ENTITY)) {
entities.add(subclass.name());
}
}
for (ClassInfo subclass : index.getIndex().getAllKnownSubclasses(PANACHE_KOTLIN_ENTITY_BASE)) {
if (!subclass.name().equals(PANACHE_KOTLIN_ENTITY)) {
entities.add(subclass.name());
}
}
Set<DotName> entityUsers = new HashSet<>();
for (DotName entity : entities) {
for (ClassInfo user : index.getIndex().getKnownUsers(entity)) {
entityUsers.add(user.name());
}
}

annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {
@Override
public boolean appliesTo(Kind kind) {
return kind == Kind.METHOD;
}

@Override
public void transform(TransformationContext context) {
MethodInfo method = context.getTarget().asMethod();
Collection<AnnotationInstance> annotations = context.getAnnotations();
if (method.isSynthetic()
|| Modifier.isStatic(method.flags())
|| method.declaringClass().isInterface()
|| !method.returnType().name().equals(UNI)
|| !entityUsers.contains(method.declaringClass().name())
|| !Annotations.containsAny(annotations, designators)
|| Annotations.contains(annotations, REACTIVE_TRANSACTIONAL)
|| Annotations.contains(annotations, REACTIVE_SESSION)) {
return;
}
// Add @ReactiveSession to a method that
// - is not static
// - is not synthetic
// - returns Uni
// - is declared in a class that uses a panache entity
// - is annotated with @GET, @POST, @PUT, @DELETE ,@PATCH ,@HEAD or @OPTIONS
// - is not annotated with @ReactiveTransactional or @ReactiveSession
context.transform().add(REACTIVE_SESSION).done();
}
}));
}
}

@BuildStep
void lookupNamedQueries(CombinedIndexBuildItem index,
BuildProducer<PanacheNamedQueryEntityClassBuildStep> namedQueries,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.hibernate.reactive.panache.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.interceptor.InterceptorBinding;

import org.hibernate.reactive.mutiny.Mutiny;

import io.smallrye.mutiny.Uni;

/**
* Instructs Panache to invoke the intercepted method within a scope of a reactive {@link Mutiny.Session}.
* <p>
* An existing reactive {@link Mutiny.Session} is reused. If no session exists then a new session is opened lazily and
* eventually closed when the {@link Uni} returned from the annotated method completes.
* <p>
* A method annotated with this annotation must return {@link Uni}. If declared on a class then all methods that are intercepted
* must return {@link Uni}.
*/
@Inherited
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface ReactiveSession {

}
Loading

0 comments on commit 50a181f

Please sign in to comment.