Skip to content

Commit

Permalink
Allow Arc to deal with final methods of beans that needs to be proxied
Browse files Browse the repository at this point in the history
This is done in the same manner that is already present for
interceptors and is very useful for Kotlin code where methods
are final by default

Fixes: quarkusio#10290
  • Loading branch information
geoand committed Jul 8, 2020
1 parent b4f6b6c commit 5ee6354
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 44 deletions.
10 changes: 9 additions & 1 deletion docs/src/main/asciidoc/cdi-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,15 @@ class Services {
* Private static methods are never intercepted
* `InvocationContext#getTarget()` returns `null` for obvious reasons; therefore not all existing interceptors may behave correctly when intercepting static methods
+
NOTE: Interceptors can use `InvocationContext.getMethod()` to detect static methods and adjust the behavior accordingly.
NOTE: Interceptors can use `InvocationContext.getMethod()` to detect static methods and adjust the behavior accordingly.

=== Ability to handle 'final' classes and methods

In normal CDI, classes that are marked as `final` and / or have `final` methods are not eligible for proxy creation,
which in turn means that interceptors and normal scoped beans don't work properly.
This situation is very common when trying to use CDI with alternative JVM languages like Kotlin where classes and methods are `final` by default.

Quarkus however, can overcome these limitations when `quarkus.arc.transform-unproxyable-classes` is set to `true` (which is the default value).

== Build Time Extension Points

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public class ArcConfig {
* If set to true, the bytecode of unproxyable beans will be transformed. This ensures that a proxy/subclass
* can be created properly. If the value is set to false, then an exception is thrown at build time indicating that a
* subclass/proxy could not be created.
*
* Quarkus performs the following transformations when this setting is enabled:
* <ul>
* <li> Remove 'final' modifier from classes and methods when a proxy is required.
* <li> Create a no-args constructor if needed.
* <li> Makes private no-args constructors package-private if necessary.
* </ul
*/
@ConfigItem(defaultValue = "true")
public boolean transformUnproxyableClasses;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,12 +387,7 @@ public ValidationPhaseBuildItem validate(ObserverRegistrationPhaseBuildItem obse
configurator.getValues().forEach(ObserverConfigurator::done);
}

Consumer<BytecodeTransformer> bytecodeTransformerConsumer = new Consumer<BytecodeTransformer>() {
@Override
public void accept(BytecodeTransformer t) {
bytecodeTransformer.produce(new BytecodeTransformerBuildItem(t.getClassToTransform(), t.getVisitorFunction()));
}
};
Consumer<BytecodeTransformer> bytecodeTransformerConsumer = new BytecodeTransformerConsumer(bytecodeTransformer);
observerRegistrationPhase.getBeanProcessor().initialize(bytecodeTransformerConsumer);
return new ValidationPhaseBuildItem(observerRegistrationPhase.getBeanProcessor().validate(bytecodeTransformerConsumer),
observerRegistrationPhase.getBeanProcessor());
Expand All @@ -410,7 +405,8 @@ public BeanContainerBuildItem generateResources(ArcRecorder recorder, ShutdownCo
BuildProducer<ReflectiveFieldBuildItem> reflectiveFields,
BuildProducer<GeneratedClassBuildItem> generatedClass,
LiveReloadBuildItem liveReloadBuildItem,
BuildProducer<GeneratedResourceBuildItem> generatedResource) throws Exception {
BuildProducer<GeneratedResourceBuildItem> generatedResource,
BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformer) throws Exception {

for (ValidationErrorBuildItem validationError : validationErrors) {
for (Throwable error : validationError.getValues()) {
Expand All @@ -426,6 +422,8 @@ public BeanContainerBuildItem generateResources(ArcRecorder recorder, ShutdownCo
liveReloadBuildItem.setContextObject(ExistingClasses.class, existingClasses);
}

Consumer<BytecodeTransformer> bytecodeTransformerConsumer = new BytecodeTransformerConsumer(bytecodeTransformer);

long start = System.currentTimeMillis();
List<ResourceOutput.Resource> resources = beanProcessor.generateResources(new ReflectionRegistration() {
@Override
Expand All @@ -437,7 +435,7 @@ public void registerMethod(MethodInfo methodInfo) {
public void registerField(FieldInfo fieldInfo) {
reflectiveFields.produce(new ReflectiveFieldBuildItem(fieldInfo));
}
}, existingClasses.existingClasses);
}, existingClasses.existingClasses, bytecodeTransformerConsumer);
for (ResourceOutput.Resource resource : resources) {
switch (resource.getType()) {
case JAVA_CLASS:
Expand Down Expand Up @@ -603,4 +601,18 @@ public boolean test(T t) {
static class ExistingClasses {
Set<String> existingClasses = new HashSet<>();
}

private static class BytecodeTransformerConsumer implements Consumer<BytecodeTransformer> {

private final BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformer;

public BytecodeTransformerConsumer(BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformer) {
this.bytecodeTransformer = bytecodeTransformer;
}

@Override
public void accept(BytecodeTransformer t) {
bytecodeTransformer.produce(new BytecodeTransformerBuildItem(t.getClassToTransform(), t.getVisitorFunction()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.quarkus.arc.test.unproxyable;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.annotation.PostConstruct;
import javax.enterprise.context.Dependent;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.inject.Singleton;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.ManagedContext;
import io.quarkus.test.QuarkusUnitTest;

public class RequestScopedFinalMethodsTest {

@RegisterExtension
public static QuarkusUnitTest container = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(RequestScopedBean.class));

@Test
public void testRequestScopedBeanWorksProperly() {
ArcContainer container = Arc.container();
ManagedContext requestContext = container.requestContext();
requestContext.activate();

InstanceHandle<RequestScopedBean> handle = container.instance(RequestScopedBean.class);
Assertions.assertTrue(handle.isAvailable());

RequestScopedBean bean = handle.get();
Assertions.assertNull(bean.getProp());
bean.setProp(100);
Assertions.assertEquals(100, bean.getProp());

requestContext.terminate();
requestContext.activate();

handle = container.instance(RequestScopedBean.class);
bean = handle.get();
Assertions.assertTrue(handle.isAvailable());
Assertions.assertNull(bean.getProp());
}

@RequestScoped
static class RequestScopedBean {
private Integer prop = null;

public final Integer getProp() {
return prop;
}

public final void setProp(Integer prop) {
this.prop = prop;
}
}

@Dependent
static class StringProducer {

@Inject
Event<String> event;

void fire(String value) {
event.fire(value);
}

}

@Singleton
static class StringObserver {

private List<Integer> events;

@Inject
RequestScopedBean requestScopedBean;

@PostConstruct
void init() {
events = new CopyOnWriteArrayList<>();
}

void observeSync(@Observes Integer value) {
Integer oldValue = requestScopedBean.getProp();
Integer newValue = oldValue == null ? value : value + oldValue;
requestScopedBean.setProp(newValue);
events.add(newValue);
}

List<Integer> getEvents() {
return events;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* <li>{@link #initialize(Consumer)}</li>
* <li>{@link #validate(Consumer)}</li>
* <li>{@link #processValidationErrors(io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext)}</li>
* <li>{@link #generateResources(ReflectionRegistration, Set)}</li>
* <li>{@link #generateResources(ReflectionRegistration, Set, Consumer)}</li>
* </ol>
*/
public class BeanProcessor {
Expand All @@ -64,6 +64,7 @@ public static Builder builder() {
private final BeanDeployment beanDeployment;
private final boolean generateSources;
private final boolean allowMocking;
private final boolean transformUnproxyableClasses;

// This predicate is used to filter annotations for InjectionPoint metadata
// Note that we do create annotation literals for all annotations for an injection point that resolves to a @Dependent bean that injects the InjectionPoint metadata
Expand All @@ -79,6 +80,7 @@ private BeanProcessor(Builder builder) {
this.annotationLiterals = new AnnotationLiteralProcessor(builder.sharedAnnotationLiterals, applicationClassPredicate);
this.generateSources = builder.generateSources;
this.allowMocking = builder.allowMocking;
this.transformUnproxyableClasses = builder.transformUnproxyableClasses;

// Initialize all build processors
buildContext = new BuildContextImpl();
Expand Down Expand Up @@ -133,7 +135,8 @@ public void processValidationErrors(BeanDeploymentValidator.ValidationContext va
BeanDeployment.processErrors(validationContext.getDeploymentProblems());
}

public List<Resource> generateResources(ReflectionRegistration reflectionRegistration, Set<String> existingClasses)
public List<Resource> generateResources(ReflectionRegistration reflectionRegistration, Set<String> existingClasses,
Consumer<BytecodeTransformer> bytecodeTransformerConsumer)
throws IOException {
if (reflectionRegistration == null) {
reflectionRegistration = this.reflectionRegistration;
Expand Down Expand Up @@ -174,7 +177,8 @@ public List<Resource> generateResources(ReflectionRegistration reflectionRegistr
if (bean.getScope().isNormal()) {
// Generate client proxy
resources.addAll(
clientProxyGenerator.generate(bean, resource.getFullyQualifiedName()));
clientProxyGenerator.generate(bean, resource.getFullyQualifiedName(),
bytecodeTransformerConsumer, transformUnproxyableClasses));
}
if (bean.isSubclassRequired()) {
resources.addAll(
Expand Down Expand Up @@ -234,7 +238,7 @@ public void accept(BytecodeTransformer transformer) {
initialize(unsupportedBytecodeTransformer);
ValidationContext validationContext = validate(unsupportedBytecodeTransformer);
processValidationErrors(validationContext);
generateResources(null, new HashSet<>());
generateResources(null, new HashSet<>(), unsupportedBytecodeTransformer);
return beanDeployment;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.enterprise.context.ContextNotActiveException;
import javax.enterprise.context.spi.Contextual;
Expand Down Expand Up @@ -70,9 +72,12 @@ public ClientProxyGenerator(Predicate<DotName> applicationClassPredicate, boolea
*
* @param bean
* @param beanClassName Fully qualified class name
* @param bytecodeTransformerConsumer
* @param transformUnproxyableClasses whether or not unproxyable classes should be transformed
* @return a collection of resources
*/
Collection<Resource> generate(BeanInfo bean, String beanClassName) {
Collection<Resource> generate(BeanInfo bean, String beanClassName,
Consumer<BytecodeTransformer> bytecodeTransformerConsumer, boolean transformUnproxyableClasses) {

ResourceClassOutput classOutput = new ResourceClassOutput(applicationClassPredicate.test(bean.getBeanClass()),
generateSources);
Expand Down Expand Up @@ -127,7 +132,7 @@ Collection<Resource> generate(BeanInfo bean, String beanClassName) {
implementMockMethods(clientProxy);
}

for (MethodInfo method : getDelegatingMethods(bean)) {
for (MethodInfo method : getDelegatingMethods(bean, bytecodeTransformerConsumer, transformUnproxyableClasses)) {

MethodDescriptor originalMethodDescriptor = MethodDescriptor.of(method);
MethodCreator forward = clientProxy.getMethodCreator(originalMethodDescriptor);
Expand Down Expand Up @@ -302,23 +307,35 @@ void implementGetBean(ClassCreator clientProxy, FieldDescriptor beanField) {
creator.returnValue(creator.readInstanceField(beanField, creator.getThis()));
}

Collection<MethodInfo> getDelegatingMethods(BeanInfo bean) {
Collection<MethodInfo> getDelegatingMethods(BeanInfo bean, Consumer<BytecodeTransformer> bytecodeTransformerConsumer,
boolean transformUnproxyableClasses) {
Map<Methods.MethodKey, MethodInfo> methods = new HashMap<>();

if (bean.isClassBean()) {
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), bean.getTarget().get().asClass(),
methods);
Set<Methods.NameAndDescriptor> methodsFromWhichToRemoveFinal = new HashSet<>();
ClassInfo classInfo = bean.getTarget().get().asClass();
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), classInfo,
methods, methodsFromWhichToRemoveFinal, transformUnproxyableClasses);
if (!methodsFromWhichToRemoveFinal.isEmpty()) {
String className = classInfo.name().toString();
bytecodeTransformerConsumer.accept(new BytecodeTransformer(className,
new Methods.RemoveFinalFromMethod(className, methodsFromWhichToRemoveFinal)));
}
} else if (bean.isProducerMethod()) {
MethodInfo producerMethod = bean.getTarget().get().asMethod();
ClassInfo returnTypeClass = getClassByName(bean.getDeployment().getIndex(), producerMethod.returnType());
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), returnTypeClass, methods);
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), returnTypeClass, methods, null,
transformUnproxyableClasses);
} else if (bean.isProducerField()) {
FieldInfo producerField = bean.getTarget().get().asField();
ClassInfo fieldClass = getClassByName(bean.getDeployment().getIndex(), producerField.type());
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), fieldClass, methods);
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), fieldClass, methods, null,
transformUnproxyableClasses);
} else if (bean.isSynthetic()) {
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), bean.getImplClazz(), methods);
Methods.addDelegatingMethods(bean.getDeployment().getIndex(), bean.getImplClazz(), methods, null,
transformUnproxyableClasses);
}

return methods.values();
}

Expand Down
Loading

0 comments on commit 5ee6354

Please sign in to comment.