Support for bean producers in different package than beans that have … (#2241)

* Support for bean producers in different package than beans that have package local methods.
* Update to proxy services
- name is generated with super interface first
- proxy classes are defined using private lookup
- proxy classes are defined using a class loader for weld specific packages

Signed-off-by: Tomas Langer <tomas.langer@oracle.com>
This commit is contained in:
Tomas Langer
2020-08-13 20:54:51 +02:00
committed by GitHub
parent bffda9677d
commit 4dfb573d17
23 changed files with 298 additions and 53 deletions

View File

@@ -33,5 +33,5 @@ module io.helidon.health.checks {
exports io.helidon.health.checks;
// required for CDI
opens io.helidon.health.checks to weld.core.impl;
opens io.helidon.health.checks to weld.core.impl, io.helidon.microprofile.cdi;
}

View File

@@ -29,7 +29,7 @@ module io.helidon.microprofile.accesslog {
exports io.helidon.microprofile.accesslog;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.accesslog to weld.core.impl;
opens io.helidon.microprofile.accesslog to weld.core.impl, io.helidon.microprofile.cdi;
provides Extension with io.helidon.microprofile.accesslog.AccessLogCdiExtension;
}

View File

@@ -23,6 +23,7 @@ import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.common.NativeImageHelper;
@@ -32,6 +33,9 @@ import org.jboss.weld.serialization.spi.ProxyServices;
class HelidonProxyServices implements ProxyServices {
private static final Logger LOGGER = Logger.getLogger(HelidonProxyServices.class.getName());
private static final String WELD_JAVAX_PREFIX = "org.jboss.weldx.";
private static final String WELD_JAVA_PREFIX = "org.jboss.weld.";
// a cache of all classloaders (this should be empty in most cases, as we use a single class loader in Helidon)
private final Map<ClassLoader, ClassDefiningCl> classLoaders = Collections.synchronizedMap(new IdentityHashMap<>());
private final ClassLoader contextCl;
@@ -69,14 +73,20 @@ class HelidonProxyServices implements ProxyServices {
public Class<?> defineClass(Class<?> originalClass, String className, byte[] classBytes, int off, int len)
throws ClassFormatError {
if (samePackage(originalClass, className)) {
// when we need to define a class in the same package (to see package local fields and methods)
// we cannot use a classloader, as the new class would be in the same package, but in a different
// classloader, preventing it from seeing these fields/methods
return defineClassSamePackage(originalClass, className, classBytes, off, len);
} else {
// use a custom classloader to define classes in a new package
return wrapCl(originalClass.getClassLoader()).doDefineClass(originalClass, className, classBytes, off, len);
if (weldInternalProxyClassName(className)) {
// this is special case - these classes are defined in a non-existent package
// and we need to use a classloader (public lookup will not allow this, and private lookup is not
// possible for an empty package)
return wrapCl(originalClass.getClassLoader())
.doDefineClass(originalClass, className, classBytes, off, len);
}
// any other class should be defined using a private lookup
try {
return defineClassPrivateLookup(originalClass, className, classBytes, off, len);
} catch (Exception e) {
LOGGER.log(Level.FINEST, "Failed to create class " + className + " using private lookup", e);
throw e;
}
}
@@ -88,12 +98,21 @@ class HelidonProxyServices implements ProxyServices {
int len,
ProtectionDomain protectionDomain) throws ClassFormatError {
if (samePackage(originalClass, className)) {
return defineClassSamePackage(originalClass, className, classBytes, off, len);
} else {
if (weldInternalProxyClassName(className)) {
// this is special case - these classes are defined in a non-existent package
// and we need to use a classloader (public lookup will not allow this, and private lookup is not
// possible for an empty package)
return wrapCl(originalClass.getClassLoader())
.doDefineClass(originalClass, className, classBytes, off, len, protectionDomain);
}
// any other class should be defined using a private lookup
try {
return defineClassPrivateLookup(originalClass, className, classBytes, off, len);
} catch (Exception e) {
LOGGER.log(Level.FINEST, "Failed to create class " + className + " using private lookup", e);
throw e;
}
}
@Override
@@ -101,7 +120,11 @@ class HelidonProxyServices implements ProxyServices {
return wrapCl(originalClass.getClassLoader()).loadClass(classBinaryName);
}
private Class<?> defineClassSamePackage(Class<?> originalClass, String className, byte[] classBytes, int off, int len) {
private boolean weldInternalProxyClassName(String className) {
return className.startsWith(WELD_JAVAX_PREFIX) || className.startsWith(WELD_JAVA_PREFIX);
}
private Class<?> defineClassPrivateLookup(Class<?> originalClass, String className, byte[] classBytes, int off, int len) {
if (NativeImageHelper.isRuntime()) {
throw new IllegalStateException("Cannot define class in native image. Class name: " + className + ", original "
+ "class: " + originalClass
@@ -110,39 +133,64 @@ class HelidonProxyServices implements ProxyServices {
LOGGER.finest("Defining class " + className + " original class: " + originalClass.getName());
MethodHandles.Lookup lookup;
try {
Module classModule = originalClass.getModule();
if (!myModule.canRead(classModule)) {
// lookup class name "guessed" from the class name of the proxy
// proxy name must contain the $ - if it does not, we just use the originalClass as that is safe
int index = className.indexOf('$');
Class<?> lookupClass;
if (index < 0) {
LOGGER.finest(() -> "Attempt to define a proxy class without a $ in its name. Class name: " + className + ","
+ " original class name: " + originalClass.getName());
lookupClass = originalClass;
} else {
// I would like to create a private lookup in the same package as the proxied class, so let's do it
// use the "extracted" lookup class name, if that fails, use the original class
lookupClass = tryLoading(originalClass, className.substring(0, index));
}
Module lookupClassModule = lookupClass.getModule();
if (!myModule.canRead(lookupClassModule)) {
// we need to read the module to be able to create a private lookup in it
// it also needs to open the package we are doing the lookup in
myModule.addReads(classModule);
myModule.addReads(lookupClassModule);
}
// next line would fail if the module does not open its package, with a very meaningful error message
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(originalClass, MethodHandles.lookup());
if (classBytes.length == len) {
return lookup.defineClass(classBytes);
} else {
byte[] bytes = new byte[len];
System.arraycopy(classBytes, off, bytes, 0, len);
return lookup.defineClass(bytes);
}
lookup = MethodHandles.privateLookupIn(lookupClass, MethodHandles.lookup());
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to define class " + className, e);
}
return defineClass(lookup, className, classBytes, off, len);
}
private boolean samePackage(Class<?> originalClass, String className) {
String origPackage = originalClass.getPackageName();
String newPackage = packageName(className);
return newPackage.equals(origPackage);
}
private String packageName(String className) {
int index = className.lastIndexOf('.');
if (index > 0) {
return className.substring(0, index);
private Class<?> tryLoading(Class<?> originalClass, String className) {
try {
return originalClass.getClassLoader().loadClass(className);
} catch (Exception e) {
LOGGER.log(Level.FINEST, "Attempt to load class " + className + " failed.", e);
return originalClass;
}
}
private Class<?> defineClass(MethodHandles.Lookup lookup, String className, byte[] classBytes, int off, int len) {
try {
byte[] definitionBytes;
if (classBytes.length == len) {
definitionBytes = classBytes;
} else {
definitionBytes = new byte[len];
System.arraycopy(classBytes, off, definitionBytes, 0, len);
}
return lookup.defineClass(definitionBytes);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to define class " + className, e);
}
return "";
}
private ClassDefiningCl wrapCl(ClassLoader origCl) {

View File

@@ -31,7 +31,7 @@ module io.helidon.microprofile.config {
exports io.helidon.microprofile.config;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.config to weld.core.impl;
opens io.helidon.microprofile.config to weld.core.impl, io.helidon.microprofile.cdi;
provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.config.ConfigCdiExtension;
}

View File

@@ -42,7 +42,7 @@ module io.helidon.microprofile.faulttolerance {
exports io.helidon.microprofile.faulttolerance;
// needed when running with modules - to make private methods accessible
opens io.helidon.microprofile.faulttolerance to weld.core.impl;
opens io.helidon.microprofile.faulttolerance to weld.core.impl, io.helidon.microprofile.cdi;
provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.faulttolerance.FaultToleranceExtension;
}

View File

@@ -38,7 +38,7 @@ module io.helidon.microprofile.health {
exports io.helidon.microprofile.health;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.health to weld.core.impl;
opens io.helidon.microprofile.health to weld.core.impl, io.helidon.microprofile.cdi;
uses io.helidon.microprofile.health.HealthCheckProvider;

View File

@@ -41,7 +41,7 @@ module io.helidon.microprofile.jwt.auth {
exports io.helidon.microprofile.jwt.auth;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.jwt.auth to weld.core.impl;
opens io.helidon.microprofile.jwt.auth to weld.core.impl, io.helidon.microprofile.cdi;
provides io.helidon.security.providers.common.spi.AnnotationAnalyzer with io.helidon.microprofile.jwt.auth.JwtAuthAnnotationAnalyzer;
provides io.helidon.security.spi.SecurityProviderService with io.helidon.microprofile.jwt.auth.JwtAuthProviderService;

View File

@@ -33,7 +33,7 @@ module io.helidon.microprofile.openapi {
exports io.helidon.microprofile.openapi;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.openapi to weld.core.impl;
opens io.helidon.microprofile.openapi to weld.core.impl, io.helidon.microprofile.cdi;
provides Extension with OpenApiCdiExtension;
}

View File

@@ -31,7 +31,7 @@ module io.helidon.microprofile.security {
exports io.helidon.microprofile.security;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.security to weld.core.impl;
opens io.helidon.microprofile.security to weld.core.impl, io.helidon.microprofile.cdi;
provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.security.SecurityCdiExtension;
}

View File

@@ -46,5 +46,5 @@ module io.helidon.microprofile.server {
io.helidon.microprofile.server.JaxRsCdiExtension;
// needed when running with modules - to make private methods accessible
opens io.helidon.microprofile.server to weld.core.impl;
opens io.helidon.microprofile.server to weld.core.impl, io.helidon.microprofile.cdi;
}

View File

@@ -47,7 +47,7 @@ module io.helidon.microprofile.tracing {
exports io.helidon.microprofile.tracing;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.tracing to weld.core.impl,hk2.utils;
opens io.helidon.microprofile.tracing to weld.core.impl,hk2.utils, io.helidon.microprofile.cdi;
provides Extension with io.helidon.microprofile.tracing.TracingCdiExtension;
provides org.glassfish.jersey.internal.spi.AutoDiscoverable with io.helidon.microprofile.tracing.MpTracingAutoDiscoverable;

View File

@@ -36,7 +36,7 @@ module io.helidon.microprofile.tyrus {
exports io.helidon.microprofile.tyrus;
// this is needed for CDI extensions that use non-public observer methods
opens io.helidon.microprofile.tyrus to weld.core.impl;
opens io.helidon.microprofile.tyrus to weld.core.impl, io.helidon.microprofile.cdi;
provides javax.enterprise.inject.spi.Extension with io.helidon.microprofile.tyrus.WebSocketCdiExtension;
}

View File

@@ -79,13 +79,16 @@ import static org.jboss.weld.util.reflection.Reflections.cast;
/*
* This class is copied from Weld sources.
* The only modified method is createCompoundProxyName.
* Modified methods:
* - getProxyName
* - createCompoundProxyName
*
* Why?
* In original Weld, the name is generated with bean identifier that is based on identity hashCode - and that is OK as
* long as you run in a single JVM (which is the case with hotspot).
* When running in native image, we go through the process of generating proxies at build time (in GraalVM when building the
* native image) and then running them from the native image.
* As these are two separate instances of JVM, we get different identity has codes, and as a result different class names
* As these are two separate instances of JVM, we get different identity hash codes, and as a result different class names
* at compile time and at runtime. The Helidon change ensures these names are equal and we can reuse the generated proxy
* classes.
*
@@ -271,8 +274,14 @@ public class ProxyFactory<T> implements PrivilegedAction<T> {
if (typeInfo.getSuperClass() == Object.class) {
final StringBuilder name = new StringBuilder();
//interface only bean.
className = createCompoundProxyName(contextId, bean, typeInfo, name) + PROXY_SUFFIX;
// for classes that do not have an enclosing class, we want the super interface to be first
if (proxiedBeanType.getEnclosingClass() == null) {
return createProxyName(typeInfo) + PROXY_SUFFIX;
} else {
//interface only bean.
className = createCompoundProxyName(contextId, bean, typeInfo, name) + PROXY_SUFFIX;
}
} else {
boolean typeModified = false;
for (Class<?> iface : typeInfo.getInterfaces()) {
@@ -308,6 +317,33 @@ public class ProxyFactory<T> implements PrivilegedAction<T> {
/*
* Helidon modification
*
* This is used when there is no enclosing type and we may have multiple interfaces
* This method ensures the superinterface is the base of the name
*/
private static String createProxyName(TypeInfo typeInfo) {
Class<?> superInterface = typeInfo.getSuperInterface();
StringBuilder name = new StringBuilder();
List<String> interfaces = new ArrayList<String>();
for (Class<?> type : typeInfo.getInterfaces()) {
if (!type.equals(superInterface)) {
interfaces.add(uniqueName(type));
}
}
Collections.sort(interfaces);
for (final String iface : interfaces) {
name.append(iface);
name.append('$');
}
return superInterface.getName() + '$' + name;
}
/*
* Helidon modification
*
* Compound name is used when more than one interface needs to be proxied.
*/
private static String createCompoundProxyName(String contextId, Bean<?> bean, TypeInfo typeInfo, StringBuilder name) {
final List<String> interfaces = new ArrayList<String>();

View File

@@ -39,7 +39,7 @@ module io.helidon.security.integration.jersey {
exports io.helidon.security.integration.jersey;
// needed for jersey injection
opens io.helidon.security.integration.jersey to hk2.locator,hk2.utils,weld.core.impl;
opens io.helidon.security.integration.jersey to hk2.locator,hk2.utils,weld.core.impl, io.helidon.microprofile.cdi;
uses io.helidon.security.providers.common.spi.AnnotationAnalyzer;
}

View File

@@ -43,7 +43,7 @@ module io.helidon.security {
exports io.helidon.security.internal to io.helidon.security.integration.jersey, io.helidon.security.integration.webserver, io.helidon.security.integration.grpc;
// needed for CDI integration
opens io.helidon.security to weld.core.impl;
opens io.helidon.security to weld.core.impl, io.helidon.microprofile.cdi;
uses io.helidon.security.spi.SecurityProviderService;
}

View File

@@ -21,7 +21,7 @@ module io.helidon.tests.apps.bookstore.common {
requires jakarta.enterprise.cdi.api;
opens io.helidon.tests.apps.bookstore.common to weld.core.impl;
opens io.helidon.tests.apps.bookstore.common to weld.core.impl, io.helidon.microprofile.cdi;
exports io.helidon.tests.apps.bookstore.common;
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.tests.integration.nativeimage.mp1;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import io.helidon.tests.integration.nativeimage.mp1.other.ProducedBean;
@ApplicationScoped
public class BeanProducer {
public static final String VALUE = "hi there";
@Produces
@ApplicationScoped
public ProducedBean produceBean() {
return new ProducedBean(VALUE);
}
}

View File

@@ -177,6 +177,9 @@ public final class Mp1Main {
// CDI - (tested indirectly by other tests)
// Server - capability to start JAX-RS (tested indirectly by other tests)
// produce a bean with package local method
invoke(collector, "Produced bean", BeanProducer.VALUE, aBean::produced);
// Configuration
invoke(collector, "Config injection", "Properties message", aBean::config);

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2020 Oracle and/or its affiliates.
*/
package io.helidon.tests.integration.nativeimage.mp1;
@@ -14,6 +14,8 @@ import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import io.helidon.microprofile.server.ServerCdiExtension;
import io.helidon.tests.integration.nativeimage.mp1.other.BeanProcessor;
import io.helidon.tests.integration.nativeimage.mp1.other.ProducedBean;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.faulttolerance.Asynchronous;
@@ -40,6 +42,9 @@ public class TestBean {
@Inject
private BeanManager beanManager;
@Inject
private ProducedBean producedBean;
private final AtomicInteger retries = new AtomicInteger();
@Timed
@@ -109,4 +114,8 @@ public class TestBean {
public CompletionStage<String> asynchronous() {
return CompletableFuture.completedFuture("Async response");
}
public String produced() {
return BeanProcessor.getProducedName(producedBean);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.tests.integration.nativeimage.mp1.other;
public final class BeanProcessor {
public static String getProducedName(ProducedBean bean) {
Class<?> sampleClass = ProducedBean.class;
Class<?> proxyClass = bean.getClass();
Package samplePackage = sampleClass.getPackage();
Package proxyPackage = proxyClass.getPackage();
System.out.println(samplePackage);
System.out.println(proxyPackage);
System.out.println("Equals: " + samplePackage.equals(proxyPackage));
String name = bean.getName();
return name;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.tests.integration.nativeimage.mp1.other;
import java.util.Objects;
/**
* A bean produced by a producer in a different package.
*/
public class ProducedBean {
private final String name;
/**
* Constructor to create a new instance outside of CDI.
*
* @param name name to use
*/
public ProducedBean(final String name) {
this.name = Objects.requireNonNull(name);
}
// Add public to make it work
String getName() {
return name;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ProducedBean myClass = (ProducedBean) o;
return Objects.equals(name, myClass.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This package exists to make sure we can do proxies that have working
* package local
*/
package io.helidon.tests.integration.nativeimage.mp1.other;

View File

@@ -37,9 +37,11 @@ module helidon.tests.nimage.mp {
requires io.helidon.health.checks;
exports io.helidon.tests.integration.nativeimage.mp1;
exports io.helidon.tests.integration.nativeimage.mp1.other;
// opens is needed to inject private fields, create classes in the same package (proxy)
opens io.helidon.tests.integration.nativeimage.mp1 to weld.core.impl, hk2.utils, io.helidon.microprofile.cdi;
opens io.helidon.tests.integration.nativeimage.mp1.other to weld.core.impl, io.helidon.microprofile.cdi;
// we need to open the static resource on classpath directory to everybody, as otherwise
// static content will not see it